WEBDAV(Web Distributed Authoring and Versioning) 协议在跨设备存储上非常有用,很多客户端都支持此协议,这是基于 HTTP 协议的一些扩展升级,以此来实现对目录文件实现存储读写。本文主要是记录如何实现一个 WEBDAV 协议服务端,最终你可以利用系统内置的 WEBDAV 协议,或者支持 WEBDAV 协议的客户端软件来将你的服务挂载为一块可用的网络硬盘,也可以在应用程序中进行数据的存取使用。
参考 wiki
关于 webdav 的协议简介及协议细节,在具体协议实现过程中可以查询此文档。
https://zh.wikipedia.org/wiki/%E5%9F%BA%E4%BA%8EWeb%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%96%E5%86%99%E5%92%8C%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6
WEBDAV 协议细则 Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol
http://webdav.org/specs/rfc3744.html
入手
这个文档看起来比较生硬,也很难入手,不知道如何去写,如何去验证协议有效性,同时,我也没有找到合适测试 WEBDAV 协议的工具。
PHP 还有一款 webdav 协议的软件 sabre, 这个 sabre 比较庞大,服务拆分的非常细,入门学习,问题追踪非常困难。
在这里换一种方式去理解去实现,比如先找到一款其他人可用能用的服务端,然后通过客户端去挂载访问,进行简单的目录文件预览、文件读取、文件写入删除等操作,同时呢因为是基于 HTTP 协议的你也很容易通过抓包去了解每一步操作发生了些什么。
这里推荐使用 wireshark 去进行抓包,记得将你服务端部署在其他机器上,方便通过 IP 过滤无效的数据包。
无论通过什么方式,在这里我们大概了解到了,实现 WEBDAV 协议服务端最基础的几个操作就是:
PUT 创建写入文件
DELETE 删除文件
GET 获取文件内容
PROPFIND 获取文件或目录列表信息
实现了上面的,应该就能实现基本的硬盘挂载了吧,下面开始进行实验。
由于是 HTTP 协议,所以也是基于 web 服务的,不需要我们做过多的其他配置。
新建一个工作目录:
由于 PHP 本身已经具有 HTTP 协议解析服务,在这里我们直接通过 PHP 内置功能创建一个 web 服务:
php -S 0.0.0.0:9999
然后在工作目录中创建 php 文件,就可以通过浏览器访问到。
创建一个 webdav.php 文件,并通过浏览器访问,确保没有问题。
实现基础类
首先实现一个基础的类:
<?phpclass dav{ public function options() { } public function head() { } public function get() { } public function put() { } public function propfind() { } public function delete() { }}$dav = new dav();$request_method = strtolower($_SERVER['REQUEST_METHOD']);if (method_exists($dav, $request_method)) { $dav->$request_method();} else { // 405 Method Not Allowed}
当 PHP 接收到请求,会根据具体的请求方法执行到对应的类方法。
根据抓包,我们发现 windows 尝试连接 我们服务时,还会请求一次 options 操作,返回当前服务允许访问的方法:
public function options() { header('Allow: OPTIONS, GET, PUT, PROPFIND, PROPPATCH'); // Allow: OPTIONS, GET, PUT, PROPFIND, PROPPATCH, ACL response_http_code(200); }
同时,我们还发现 当文件目录不存在时,或者出现错误还会返回一些其他 HTTP 状态码,同时这也是 WEBDAV 服务最基础的一些协商,当资源或请求存在问题时,会返回对应的 HTTP 状态码。
简单实现一下:
function http_code($num){ $http = array( 100 => "HTTP/1.1 100 Continue", 101 => "HTTP/1.1 101 Switching Protocols", 200 => "HTTP/1.1 200 OK", 201 => "HTTP/1.1 201 Created", 202 => "HTTP/1.1 202 Accepted", 203 => "HTTP/1.1 203 Non-Authoritative Information", 204 => "HTTP/1.1 204 No Content", 205 => "HTTP/1.1 205 Reset Content", 206 => "HTTP/1.1 206 Partial Content", 207 => "HTTP/1.1 207 Multi-Status", 300 => "HTTP/1.1 300 Multiple Choices", 301 => "HTTP/1.1 301 Moved Permanently", 302 => "HTTP/1.1 302 Found", 303 => "HTTP/1.1 303 See Other", 304 => "HTTP/1.1 304 Not Modified", 305 => "HTTP/1.1 305 Use Proxy", 307 => "HTTP/1.1 307 Temporary Redirect", 400 => "HTTP/1.1 400 Bad Request", 401 => "HTTP/1.1 401 Unauthorized", 402 => "HTTP/1.1 402 Payment Required", 403 => "HTTP/1.1 403 Forbidden", 404 => "HTTP/1.1 404 Not Found", 405 => "HTTP/1.1 405 Method Not Allowed", 406 => "HTTP/1.1 406 Not Acceptable", 407 => "HTTP/1.1 407 Proxy Authentication Required", 408 => "HTTP/1.1 408 Request Time-out", 409 => "HTTP/1.1 409 Conflict", 410 => "HTTP/1.1 410 Gone", 411 => "HTTP/1.1 411 Length Required", 412 => "HTTP/1.1 412 Precondition Failed", 413 => "HTTP/1.1 413 Request Entity Too Large", 414 => "HTTP/1.1 414 Request-URI Too Large", 415 => "HTTP/1.1 415 Unsupported Media Type", 416 => "HTTP/1.1 416 Requested range not satisfiable", 417 => "HTTP/1.1 417 Expectation Failed", 500 => "HTTP/1.1 500 Internal Server Error", 501 => "HTTP/1.1 501 Not Implemented", 502 => "HTTP/1.1 502 Bad Gateway", 503 => "HTTP/1.1 503 Service Unavailable", 504 => "HTTP/1.1 504 Gateway Time-out" ); return $http[$num];}function response_http_code($num){ header(http_code($num));}
并将入口处,加入 405 状态码响应:
$dav = new dav();$request_method = strtolower($_SERVER['REQUEST_METHOD']);if (method_exists($dav, $request_method)) { $dav->$request_method();} else { // 405 Method Not Allowed response_http_code(405);}
新增一个构造方法,并将我们的存储目录设为 public:
protected $public; public function __construct() { $this->public = __DIR__.'/public'; }
PROPFIND 实现目录文件列表
PROPFIND 方法一般返回的文件属性详情,同时也可以返回目录的文件列表及目录本身的属性。
rfc3744 中的请求体和响应内容参考:
http://webdav.org/specs/rfc3744.html#n-example--retrieving-dav-owne
>> Request << PROPFIND /papers/ HTTP/1.1 Host: www.example.com Content-type: text/xml; charset="utf-8" Content-Length: xxx Depth: 0 Authorization: Digest username="jim", realm="[email protected]", nonce="...", uri="/papers/", response="...", opaque="..." <?xml version="1.0" encoding="utf-8" ?> <D:propfind xmlns:D="DAV:"> <D:prop> <D:owner/> </D:prop> </D:propfind> >> Response << HTTP/1.1 207 Multi-Status Content-Type: text/xml; charset="utf-8" Content-Length: xxx <?xml version="1.0" encoding="utf-8" ?> <D:multistatus xmlns:D="DAV:"> <D:response> <D:href>http://www.example.com/papers/</D:href> <D:propstat> <D:prop> <D:owner> <D:href>http://www.example.com/acl/users/gstein</D:href> </D:owner> </D:prop> <D:status>HTTP/1.1 200 OK</D:status> </D:propstat> </D:response> </D:multistatus>
我们接下来实现这个请求方法的返回内容就行了。
WEBDAV 协议传输数据都是通过 XML 数据返回,我从其他 WEBDAV 服务中复制了一份 PROPFIND 返回的内容,大概类似这样子的:
<?xml version="1.0" encoding="utf-8"?><D:multistatus xmlns:D="DAV:"> <D:response> <D:href>/dav/</D:href> <D:propstat> <D:prop> <D:supportedlock> <D:lockentry> <D:lockscope> <D:exclusive/> </D:lockscope> <D:locktype> <D:write/> </D:locktype> </D:lockentry> </D:supportedlock> <D:resourcetype> <D:collection></D:collection> </D:resourcetype> <D:getlastmodified>Sun, 11 Apr 2021 16:23:30 GMT</D:getlastmodified> <D:displayname/> </D:prop> <D:status>HTTP/1.1 200 OK</D:status> </D:propstat> </D:response> <D:response> <D:href>/dav/%E6%96%B0%E5%BB%BA%E6%96%87%E6%9C%AC%E6%96%87%E6%A1%A3.txt</D:href> <D:propstat> <D:prop> <D:supportedlock> <D:lockentry> <D:lockscope> <D:exclusive/> </D:lockscope> <D:locktype> <D:write/> </D:locktype> </D:lockentry> </D:supportedlock> <D:resourcetype/> <D:getcontentlength>0</D:getcontentlength> <D:getetag>"167508a952fb5c180"</D:getetag> <D:getcontenttype/> <D:displayname/> <D:getlastmodified>Mon, 12 Apr 2021 06:32:44 GMT</D:getlastmodified> </D:prop> <D:status>HTTP/1.1 200 OK</D:status> </D:propstat> </D:response></D:multistatus>
通过查看已有的返回内容,这个返回内容中只有一个文件,并且还包含了目录本身的属性信息。
那么我们也按照其规律将目录的本身和目录下的文件一并返回。
首先创建一个简单的 文件 XML 信息构建函数,为了方便我们直接通过变量替换,不用 XML 对象去做。
function response_basedir($dir, $lastmod, $status) { $lastmod = gmdate("D, d M Y H:i:s", $lastmod)." GMT"; $fmt = <<<EOF<d:response> <d:href>{$dir}</d:href> <d:propstat> <d:prop> <d:getlastmodified>{$lastmod}</d:getlastmodified> <d:resourcetype> <d:collection/> </d:resourcetype> </d:prop> <d:status>{$status}</d:status> </d:propstat> </d:response>EOF; // /dav/ //Sun, 11 Apr 2021 16:23:30 GMT // HTTP/1.1 200 OK return $fmt; } function response_dir($dir, $lastmod, $status) { $lastmod = gmdate("D, d M Y H:i:s", $lastmod)." GMT"; $fmt = <<<EOF <D:response> <D:href>{$dir}</D:href> <D:propstat> <D:prop> <D:resourcetype> <D:collection></D:collection> </D:resourcetype> <D:getlastmodified>{$lastmod}</D:getlastmodified> <D:displayname/> </D:prop> <D:status>{$status}</D:status> </D:propstat> </D:response>EOF; // /dav/ //Sun, 11 Apr 2021 16:23:30 GMT // HTTP/1.1 200 OK return $fmt; } function response_file($file_path, $lastmod, $file_length, $status) { $lastmod = gmdate("D, d M Y H:i:s", $lastmod)." GMT"; $tag = md5($lastmod.$file_path); $fmt = <<<EOF <D:response> <D:href>{$file_path}</D:href> <D:propstat> <D:prop> <D:resourcetype/> <D:getcontentlength>{$file_length}</D:getcontentlength> <D:getetag>"{$tag}"</D:getetag> <D:getcontenttype/> <D:displayname/> <D:getlastmodified>{$lastmod}</D:getlastmodified> </D:prop> <D:status>{$status}</D:status> </D:propstat> </D:response>EOF; // /dav/%E6%96%B0%E5%BB%BA%E6%96%87%E6%9C%AC%E6%96%87%E6%A1%A3.txt // 0 // HTTP/1.1 200 OK // Mon, 12 Apr 2021 06:32:44 GMT return $fmt; } function response($text) { return <<<EOF<?xml version="1.0" encoding="utf-8"?><D:multistatus xmlns:D="DAV:"> {$text}</D:multistatus>EOF; }
注意这里的时间,为了标准化,我转换成了 GMT 格式,tag 简单用 md5 取修改时间和路径名得出。
除了目录 XML 外,额外添加了个 response_basedir 用于返回本级目录信息的 XML 内容,这和子目录略不一样。
为了知道请求时的参数,我们将 PROPFIND 方法暂时修改为输出 $_SERVER 的内容,方便提取自己想要的数据。
public function propfind() { var_dump($_SERVER); die; }
通过 POSTMAN 请求接口,我们可以看到如下的数据:
array(23) {["DOCUMENT_ROOT"]=>string(50) "C:\Users\Administrator\Documents\simple-webdav-php"["REMOTE_ADDR"]=>string(9) "127.0.0.1"["REMOTE_PORT"]=>string(4) "8673"["SERVER_SOFTWARE"]=>string(29) "PHP 7.4.16 Development Server"["SERVER_PROTOCOL"]=>string(8) "HTTP/1.1"["SERVER_NAME"]=>string(7) "0.0.0.0"["SERVER_PORT"]=>string(4) "9999"["REQUEST_URI"]=>string(12) "/webdav.php/"["REQUEST_METHOD"]=>string(8) "PROPFIND"["SCRIPT_NAME"]=>string(11) "/webdav.php"["SCRIPT_FILENAME"]=>string(61) "C:\Users\Administrator\Documents\simple-webdav-php\webdav.php"["PATH_INFO"]=>string(1) "/"["PHP_SELF"]=>string(12) "/webdav.php/"["HTTP_USER_AGENT"]=>string(21) "PostmanRuntime/7.26.8"["HTTP_ACCEPT"]=>string(3) "*/*"["HTTP_POSTMAN_TOKEN"]=>string(36) "e4809ebd-013a-41a7-885f-28828990ee10"["HTTP_HOST"]=>string(14) "127.0.0.1:9999"["HTTP_ACCEPT_ENCODING"]=>string(17) "gzip, deflate, br"["HTTP_CONNECTION"]=>string(10) "keep-alive"["CONTENT_LENGTH"]=>string(1) "0"["HTTP_CONTENT_LENGTH"]=>string(1) "0"["REQUEST_TIME_FLOAT"]=>float(1618726819.9309)["REQUEST_TIME"]=>int(1618726819)}
构想一下,webdav.php 为我们的服务器文件,那么请求地址应该是 http://127.0.0.1:9999/webdav.php
列出根目录的请求就应该是: PROPFIND http://127.0.0.1:9999/webdav.php
列出 mobile 的请求就应该是: PROPFIND http://127.0.0.1:9999/webdav.php/mobile
查看 mobile 目录下 test.txt 的请求应该是: GET http://127.0.0.1:9999/webdav.php/mobile/test.txt
参考上面 $_SERVER 返回的内容,我们可以通过 $_SERVER['PATH_INFO'] 拿到自己想要的目标文件相对路径,由于我们将 public 作为 webdav 服务的根目录,那么可以写出如下的代码:
public function propfind() { $path = $this->public.'/'.ltrim($_SERVER['PATH_INFO'] ?? '','/'); $dav_base_dir = $_SERVER['SCRIPT_NAME']. '/'.ltrim($_SERVER['PATH_INFO'] ?? '','/'); $files = scandir($path); $response_text = response_basedir($dav_base_dir,filemtime($path),http_code(200)); foreach ($files as $file){ if($file == '.' || $file == '..'){ continue; } $file_path = $path.'/'.$file; $mtime = filemtime($file_path); if(is_dir($file_path)){ $response_text.= response_dir($dav_base_dir.'/'.$file,$mtime,http_code(200)); }elseif(is_file($file_path)){ $response_text.= response_file($dav_base_dir.'/'.$file, $mtime,filesize($file_path),http_code(200)); } } response_http_code(207); header('Content-Type: text/xml; charset=utf-8'); echo response($response_text); }
给 public 目录创建一个文件,我们再通过 POSTMAN 请求接口:
<?xml version="1.0" encoding="utf-8"?> <D:multistatus xmlns:D="DAV:"> <D:response> <D:href>/webdav.php/</D:href> <D:propstat> <D:prop> <D:resourcetype> <D:collection></D:collection> </D:resourcetype> <D:getlastmodified>Sun, 18 Apr 2021 06:31:01 GMT</D:getlastmodified> <D:displayname/> </D:prop> <D:status>HTTP/1.1 200 OK</D:status> </D:propstat> </D:response> <D:response> <D:href>/webdav.php//新建文本文档.txt</D:href> <D:propstat> <D:prop> <D:resourcetype/> <D:getcontentlength>0</D:getcontentlength> <D:getetag>"018ef2aa2b63d83e6b1c6f2ff90fb792"</D:getetag> <D:getcontenttype/> <D:displayname/> <D:getlastmodified>Sun, 18 Apr 2021 06:31:01 GMT</D:getlastmodified> </D:prop> <D:status>HTTP/1.1 200 OK</D:status> </D:propstat> </D:response> </D:multistatus>
看样子已经可以了。
尝试将这个地址挂载到 windows10 的系统中访问,http://127.0.0.1:9999/webdav.php
但很不幸失败了:
为了查清楚发生了什么,我们将请求日志记录一下。
$dav = new dav();$request_method = strtolower($_SERVER['REQUEST_METHOD']);$header_text = "";foreach (getallheaders() as $name => $value) { $header_text.="$name: $value\n";}$input = file_get_contents("php://input");file_put_contents('./HEAD.log', $request_method.' '.$_SERVER['REQUEST_URI'].PHP_EOL.$header_text. PHP_EOL.$input.PHP_EOL,FILE_APPEND);if (method_exists($dav, $request_method)) { $dav->$request_method();} else { // 405 Method Not Allowed response_http_code(405);}
再尝试点击 下一步 ,打开产生的日志。
options /webdav.php Connection: Keep-Alive User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042 translate: f Host: 127.0.0.1:9999 propfind /webdav.php Connection: Keep-Alive User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042 Depth: 0 translate: f Content-Length: 0 Host: 127.0.0.1:9999 propfind /webdav.php Connection: Keep-Alive User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042 Depth: 0 translate: f Content-Length: 0 Host: 127.0.0.1:9999
经过对比测试,发现 Depth: 0 时,只需要返回目录本身属性信息,不需要目录下其他文件。
修改 PROPFIND 方法为:
public function propfind() { $path = $this->public.'/'.ltrim($_SERVER['PATH_INFO'] ?? '','/'); $dav_base_dir = $_SERVER['SCRIPT_NAME']. '/'.ltrim($_SERVER['PATH_INFO'] ?? '','/'); if(isset($_SERVER['HTTP_DEPTH'])){ if($_SERVER['HTTP_DEPTH'] == 0){ if(is_file($path)){ $response_text = response_file($dav_base_dir,filemtime($path),filesize($path),http_code(200)); }elseif(is_dir($path)){ $response_text = response_basedir($dav_base_dir,filemtime($path),http_code(200)); }else{ response_http_code(404); return; } response_http_code(207); header('Content-Type: text/xml; charset=utf-8'); echo response($response_text); exit; } } $files = scandir($path); $response_text = response_basedir($dav_base_dir,filemtime($path),http_code(200)); foreach ($files as $file){ if($file == '.' || $file == '..'){ continue; } $file_path = $path.'/'.$file; $mtime = filemtime($file_path); if(is_dir($file_path)){ $response_text.= response_dir($dav_base_dir.'/'.$file,$mtime,http_code(200)); }elseif(is_file($file_path)){ $response_text.= response_file($dav_base_dir.'/'.$file, $mtime,filesize($file_path),http_code(200)); } } response_http_code(207); header('Content-Type: text/xml; charset=utf-8'); echo response($response_text); }
再次点击 下一步 按钮,发现已经成功了。
GET 实现获取文件信息
GET 直接将对应的资源内容返回就行了,不需要做处理。
public function get() { header('Content-Type: application/octet-stream'); $path = $this->public.'/'.ltrim($_SERVER['PATH_INFO'],'/'); if(is_file($path)){ $fh = fopen($path,'r'); $oh = fopen('php://output','w'); stream_copy_to_stream($fh, $oh); fclose($fh); fclose($oh); }else{ response_http_code(404); } }
PUT 实现创建写入文件内容
public function put() { $input = fopen("php://input",'r'); try{ $path = $this->public.'/'.ltrim($_SERVER['PATH_INFO'],'/'); $fh = fopen($path,'w'); stream_copy_to_stream($input, $fh); fclose($fh); }catch (Throwable $throwable){ response_http_code(503); echo $throwable->getMessage(); } }
HEAD 方法返回文件大小时间信息
public function head() { header('Content-Type: application/octet-stream'); $path = $this->public.'/'.ltrim($_SERVER['PATH_INFO'],'/'); if(is_file($path)){ header('Content-Length: '.filesize($path)); $lastmod = filemtime($path); $lastmod = gmdate("D, d M Y H:i:s", $lastmod)." GMT"; header('Last-Modified: '.$lastmod); }else{ response_http_code(404); } }
DELETE 对文件资源进行删除
public function delete() { header('Content-Type: application/octet-stream'); $path = $this->public.'/'.ltrim($_SERVER['PATH_INFO'],'/'); if($path){ if(unlink($path)){ response_http_code(200); }else{ response_http_code(503); } }else{ response_http_code(404); } }
MKCOL 创建目录
当发现目录无法创建,通过提取创建目录时的请求信息用于参考:
propfind /webdav.php/%E6%96%B0%E5%BB%BA%E6%96%87%E4%BB%B6%E5%A4%B9Connection: Keep-Alive User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042Depth: 0translate: f Content-Length: 0Host: 127.0.0.1:9999mkcol /webdav.php/%E6%96%B0%E5%BB%BA%E6%96%87%E4%BB%B6%E5%A4%B9Connection: Keep-Alive User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042translate: f Content-Length: 0Host: 127.0.0.1:9999
MOVE 文件或目录更名
请求日志参考:
move /webdav.php/%E6%96%B0%E5%BB%BA%E6%96%87%E4%BB%B6%E5%A4%B9Connection: Keep-Alive User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042Destination: http://127.0.0.1:9999/webdav.php/dffdsOverwrite: Ftranslate: f Content-Length: 0Host: 127.0.0.1:9999
PROPPATCH 方法设置资源属性
当我们实现了基本的操作方法后,发现我们刚才新建的文本内容并不能进行保存。
通过查看日志,我们发现这里有一个 LOCK 操作:
lock /webdav.php/%E6%96%B0%E5%BB%BA%E6%96%87%E6%9C%AC%E6%96%87%E6%A1%A3.txt Cache-Control: no-cache Connection: Keep-Alive Pragma: no-cache Content-Type: text/xml; charset="utf-8" User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042 translate: f Timeout: Second-3600 Content-Length: 220 Host: 127.0.0.1:9999<?xml version="1.0" encoding="utf-8" ?><D:lockinfo xmlns:D="DAV:"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype><D:owner><D:href>DESKTOP-WHOAMI\Administrator</D:href></D:owner></D:lockinfo>propfind /webdav.php Connection: Keep-Alive User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042 Depth: 1 translate: f Content-Length: 0 Host: 127.0.0.1:9999 propfind /webdav.php Connection: Keep-Alive User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042 Depth: 0 translate: f Content-Length: 0 Host: 127.0.0.1:9999
我们将添加一个 LOCK 方法,不去实现它,但需要正确返回:
public function lock() { response_http_code(501); }
当再次尝试,就可以进行保存了。
此时新建操作可以进行使用了,但当外部的文件复制进来时,会提示无法读源文件或磁盘。
原因时缺失 PROPPATCH 方法,无法对资源进行属性设置。可以参考 rfc3744 进行响应体构造 http://webdav.org/specs/rfc3744.html#n-example--an-attempt-to-set-dav-owner
这里我们简单返回 403 禁止的内容:
public function proppatch() { $path = $this->public . '/' . ltrim($_SERVER['PATH_INFO'], '/'); echo <<<EOF<?xml version="1.0" encoding="utf-8" ?> <D:multistatus xmlns:D="DAV:"> <D:response> <D:href>{$path}</D:href> <D:propstat> <D:prop><D:owner/></D:prop> <D:status>HTTP/1.1 403 Forbidden</D:status> <D:responsedescription> <D:error><D:cannot-modify-protected-property/></D:error> Failure to set protected property (DAV:owner) </D:responsedescription> </D:propstat> </D:response> </D:multistatus> EOF; response_http_code(207); }
再次从外部拖放文件到 webdav 网络盘中,此时一切正常。
结束
基本到此就结束了,通过 PHP 实现了 WEBDAV 服务端一些基本的功能,也记录了一些抓包和问题排查方法,你可以通过此为基础,参考 rfc3744 将该代码完善起来使用。
但是 PHP 还存在一个问题就是,当触发 PUT 方法请求到 PHP 服务器时,PHP 默认会将所有文件数据存储到内存当中去,之后才会执行脚本,这就导致上传的文件必须小于服务器运行内存,WEBDAV 协议本身我也没有找到关于文件分块的内容,总之这点弊端很严重,对于小内存机器就很难进行大文件传输。
关于上文的所有代码放在了这里,如果有需要可以参考:https://github.com/ellermister/simple-webdav-php