1 /**
2  * Authors: Szabo Bogdan <szabobogdan@yahoo.com>
3  * Date: 2 15, 2015
4  * License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
5  * Copyright: Public Domain
6  */
7 module vibedav.base;
8 
9 public import vibedav.prop;
10 public import vibedav.ifheader;
11 public import vibedav.locks;
12 public import vibedav.http;
13 public import vibedav.davresource;
14 
15 import vibe.core.log;
16 import vibe.core.file;
17 import vibe.inet.mimetypes;
18 import vibe.inet.message;
19 import vibe.http.server;
20 
21 import std.conv : to;
22 import std.algorithm;
23 import std.file;
24 import std.path;
25 import std.digest.md;
26 import std.datetime;
27 import std.array;
28 import std..string;
29 import std.stdio;
30 import std.typecons;
31 import std.uri;
32 import std.algorithm.sorting, std.algorithm.setops;
33 
34 class DavStorage {
35 	static {
36 		DavLockList locks;
37 	}
38 }
39 
40 class DavException : Exception {
41 	HTTPStatus status;
42 	string mime;
43 
44 	///
45 	this(HTTPStatus status, string msg, string mime = "plain/text", string file = __FILE__, size_t line = __LINE__, Throwable next = null)
46 	{
47 		super(msg, file, line, next);
48 		this.status = status;
49 		this.mime = mime;
50 	}
51 
52 	///
53 	this(HTTPStatus status, string msg, Throwable next, string mime = "plain/text", string file = __FILE__, size_t line = __LINE__)
54 	{
55 		super(msg, next, file, line);
56 		this.status = status;
57 		this.mime = mime;
58 	}
59 }
60 
61 
62 enum NoticeAction {
63 	created,
64 	deleted,
65 	changed
66 }
67 
68 string stripSlashes(string path) {
69 	return path.stripBeginSlashes.stripEndSlasshes;
70 }
71 
72 string stripBeginSlashes(string path) {
73 	if(path.length > 0 && path[0] == '/')
74 		path = path[1..$];
75 
76 	if(path.length > 1 && path[0..2] == "./")
77 		path = path[2..$];
78 
79 	return path;
80 }
81 
82 string stripEndSlasshes(string path) {
83 	if(path.length > 0 && path[path.length-1] == '/')
84 		path = path[0..$-1];
85 
86 	return path;
87 }
88 
89 Path getFilePath(Path baseUrlPath, Path basePath, URL url) {
90 	string path = url.path.toString.stripSlashes;
91 	string filePath;
92 
93 	filePath = path[baseUrlPath.toString.length..$].stripSlashes;
94 
95 	return basePath ~ filePath;
96 }
97 
98 @("Basic getFilePath")
99 unittest {
100 	auto path = getFilePath(Path("test/"), Path("/base/"), URL("http://127.0.0.1/test/file.txt"));
101 	assert(path.toString == "/base/file.txt");
102 }
103 
104 string urlToString(URL url) {
105 	string name = url.path.toNativeString;
106 	return name.replace(" ", "%20");
107 }
108 
109 struct DavReport {
110 	string name;
111 	string ns;
112 }
113 
114 string reportName(DavProp reportBody) {
115 	if(reportBody[0].name == "?xml")
116 		return reportBody[1].tagName ~ ":" ~ reportBody[1].namespace;
117 
118 	return reportBody[0].tagName ~ ":" ~ reportBody[0].namespace;
119 }
120 
121 DavReport getReportProperty(T...)() {
122 	static if(T.length == 0)
123 		static assert(false, "There is no `@DavReport` attribute.");
124 	else static if( is(typeof(T[0]) == DavReport) )
125 		return T[0];
126 	else
127 		return getResourceProperty!(T[1..$]);
128 }
129 
130 bool hasDavReport(I)(string key) {
131 	bool result = false;
132 
133 	void keyExist(T...)() {
134 		static if(T.length > 0) {
135 			enum val = getReportProperty!(__traits(getAttributes, __traits(getMember, I, T[0])));
136 			enum staticKey = val.name ~ ":" ~ val.ns;
137 
138 			if(staticKey == key)
139 				result = true;
140 
141 			keyExist!(T[1..$])();
142 		}
143 	}
144 
145 	keyExist!(__traits(allMembers, I))();
146 
147 	return result;
148 }
149 
150 void getDavReport(I)(I plugin, DavRequest request, DavResponse response) {
151 	string key = request.content.reportName;
152 
153 	void getProp(T...)() {
154 		static if(T.length > 0) {
155 			enum val = getReportProperty!(__traits(getAttributes, __traits(getMember, I, T[0])));
156 			enum staticKey = val.name ~ ":" ~ val.ns;
157 
158 
159 			if(staticKey == key) {
160 				__traits(getMember, plugin, T[0])(request, response);
161 			}
162 
163 			getProp!(T[1..$])();
164 		}
165 	}
166 
167 	getProp!(__traits(allMembers, I))();
168 }
169 
170 interface IDavResourceAccess {
171 	bool exists(URL url, string username);
172 	Path[] childList(URL url, string username);
173 	bool canCreateCollection(URL url, string username);
174 	bool canCreateResource(URL url, string username);
175 
176 	void removeResource(URL url, string username);
177 	DavResource getResource(URL url, string username);
178 	DavResource createCollection(URL url, string username);
179 	DavResource createResource(URL url, string username);
180 
181 	void bindResourcePlugins(DavResource resource);
182 }
183 
184 interface IDavPlugin : IDavResourceAccess {
185 
186 	bool hasReport(URL url, string username, string name);
187 	void report(DavRequest request, DavResponse response);
188 
189 	void notice(NoticeAction action, DavResource resource);
190 
191 	@property {
192 		IDav dav();
193 		string name();
194 
195 		string[] support(URL url, string username);
196 	}
197 }
198 
199 interface IDavPluginHub {
200 	void registerPlugin(IDavPlugin plugin);
201 	bool hasPlugin(string name);
202 }
203 
204 
205 abstract class BaseDavPlugin : IDavPlugin {
206 	protected IDav _dav;
207 
208 	this(IDav dav) {
209 		dav.registerPlugin(this);
210 		_dav = dav;
211 	}
212 
213 	Path[] childList(URL url, string username) {
214 		return [];
215 	}
216 
217 	bool exists(URL url, string username) {
218 		return false;
219 	}
220 
221 	bool canCreateCollection(URL url, string username) {
222 		return false;
223 	}
224 
225 	bool canCreateResource(URL url, string username) {
226 		return false;
227 	}
228 
229 	void removeResource(URL url, string username) {
230 		throw new DavException(HTTPStatus.internalServerError, "Can't remove resource.");
231 	}
232 
233 	DavResource getResource(URL url, string username) {
234 		if(exists(url, username)) {
235 			auto resource = new DavResource(_dav, url);
236 
237 			resource.name = url.path.head.to!string;
238 			resource.username = username;
239 
240 			return resource;
241 		}
242 
243 		throw new DavException(HTTPStatus.internalServerError, "Can't get resource.");
244 	}
245 
246 	DavResource createCollection(URL url, string username) {
247 		throw new DavException(HTTPStatus.internalServerError, "Can't create collection.");
248 	}
249 
250 	DavResource createResource(URL url, string username) {
251 		throw new DavException(HTTPStatus.internalServerError, "Can't create resource.");
252 	}
253 
254 	void bindResourcePlugins(DavResource resource) { }
255 
256 	bool hasReport(URL url, string username, string name) {
257 		return false;
258 	}
259 
260 	void report(DavRequest request, DavResponse response) {
261 		throw new DavException(HTTPStatus.internalServerError, "Can't get report.");
262 	}
263 
264 	void notice(NoticeAction action, DavResource resource) {
265 
266 	}
267 
268 	@property {
269 		IDav dav() {
270 			return _dav;
271 		}
272 
273 		string[] support(URL url, string username) {
274 			return [];
275 		}
276 	}
277 }
278 
279 interface IDav : IDavResourceAccess, IDavPluginHub {
280 	void options(DavRequest request, DavResponse response);
281 	void propfind(DavRequest request, DavResponse response);
282 	void lock(DavRequest request, DavResponse response);
283 	void get(DavRequest request, DavResponse response);
284 	void put(DavRequest request, DavResponse response);
285 	void proppatch(DavRequest request, DavResponse response);
286 	void mkcol(DavRequest request, DavResponse response) ;
287 	void remove(DavRequest request, DavResponse response);
288 	void move(DavRequest request, DavResponse response);
289 	void copy(DavRequest request, DavResponse response);
290 	void report(DavRequest request, DavResponse response);
291 
292 	void notice(NoticeAction action, DavResource resource);
293 
294 	DavResource[] getResources(URL url, ulong depth, string username);
295 
296 	@property
297 	Path rootUrl();
298 	Path path(URL);
299 }
300 
301 /// The main DAV protocol implementation
302 class Dav : IDav {
303 	protected {
304 		Path _rootUrl;
305 
306 		IDavPlugin[] plugins;
307 	}
308 
309 	@property
310 	Path rootUrl() {
311 		return _rootUrl;
312 	}
313 
314 	Path path(URL url) {
315 		auto root = Path("/") ~ rootUrl;
316 
317 		if(url.path.startsWith(root)) {
318 			return url.path.relativeTo(root);
319 		}
320 
321 		throw new DavException(HTTPStatus.internalServerError, "`" ~ url.path.toString ~ "` is not a valid DAV path.");
322 	}
323 
324 	this(string rootUrl) {
325 		_rootUrl = rootUrl;
326 		_rootUrl.endsWithSlash = true;
327 		DavStorage.locks = new DavLockList;
328 	}
329 
330 	protected {
331 		DavResource getOrCreateResource(URL url, string username, out HTTPStatus status) {
332 			DavResource resource;
333 
334 			if(exists(url, username)) {
335 				resource = getResource(url, username);
336 				status = HTTPStatus.ok;
337 			} else {
338 
339 				if(!exists(url.parentURL, username))
340 					throw new DavException(HTTPStatus.conflict, "A resource cannot be created at the destination until one or more intermediate collections have been created.");
341 
342 				resource = createResource(url, username);
343 				status = HTTPStatus.created;
344 			}
345 
346 			return resource;
347 		}
348 
349 		Path checkPath(Path path) {
350 			path.endsWithSlash = true;
351 			return path;
352 		}
353 	}
354 
355 	private {
356 
357 		bool[string] defaultPropList() {
358 			bool[string] list;
359 
360 			list["creationdate:DAV:"] = true;
361 			list["displayname:DAV:"] = true;
362 			list["getcontentlength:DAV:"] = true;
363 			list["getcontenttype:DAV:"] = true;
364 			list["getetag:DAV:"] = true;
365 			list["getlastmodified:DAV:"] = true;
366 			list["resourcetype:DAV:"] = true;
367 
368 			return list;
369 		}
370 
371 		bool[string] propList(DavProp document) {
372 			bool[string] list;
373 
374 			if(document is null || "allprop" in document["propfind"])
375 				return defaultPropList;
376 
377 			auto properties = document["propfind"]["prop"];
378 
379 			if(properties.length > 0)
380 				foreach(string key, p; properties)
381 					list[p.tagName ~ ":" ~ p.namespace] = true;
382 
383 			return list;
384 		}
385 	}
386 
387 	void registerPlugin(IDavPlugin plugin) {
388 		assert(plugin !is null, "can't set null plugin");
389 		assert(!hasPlugin(plugin.name), plugin.name ~ " already added.");
390 		plugins ~= plugin;
391 	}
392 
393 	bool hasPlugin(string name) {
394 
395 		foreach_reverse(plugin; plugins)
396 			if(plugin.name == name)
397 				return true;
398 
399 		return false;
400 	}
401 
402 	void removeResource(URL url, string username) {
403 		foreach_reverse(plugin; plugins)
404 			if(plugin.exists(url, username)) {
405 				notice(NoticeAction.deleted, getResource(url, username));
406 				return plugin.removeResource(url, username);
407 			}
408 
409 		throw new DavException(HTTPStatus.notFound, "`" ~ url.toString ~ "` not found.");
410 	}
411 
412 	DavResource getResource(URL url, string username) {
413 		foreach_reverse(plugin; plugins) {
414 			if(plugin.exists(url, username)) {
415 				auto res = plugin.getResource(url, username);
416 				bindResourcePlugins(res);
417 
418 				return res;
419 			}
420 		}
421 
422 		throw new DavException(HTTPStatus.notFound, "`" ~ url.toString ~ "` not found.");
423 	}
424 
425 	DavResource[] getResources(URL url, ulong depth, string username) {
426 		DavResource[] list;
427 		DavResource[] tmpList;
428 
429 		tmpList ~= getResource(url, username);
430 
431 		while(tmpList.length > 0 && depth > 0) {
432 			auto oldLen = tmpList.length;
433 
434 			foreach(resource; tmpList) {
435 				if(resource.isCollection) {
436 					auto childList = childList(resource.url, username);
437 
438 					foreach(path; childList) {
439 						try {
440 							tmpList ~= getResource(URL("http", "", 80, Path("/") ~_rootUrl ~ path), username);
441 						} catch(DavException e) {
442 							if(e.status == HTTPStatus.notFound) {
443 								throw new DavException(HTTPStatus.internalServerError,
444 									"Resource `" ~ url.to!string ~ "` said that it has `" ~ path.toString ~ "` child but it can not be found.");
445 							} else {
446 								throw e;
447 							}
448 						}
449 					}
450 				}
451 			}
452 
453 			list ~= tmpList[0..oldLen];
454 			tmpList = tmpList[oldLen..$];
455 			depth--;
456 		}
457 
458 		list ~= tmpList;
459 
460 		return list;
461 	}
462 
463 	DavResource createCollection(URL url, string username) {
464 		foreach_reverse(plugin; plugins)
465 			if(plugin.canCreateCollection(url, username)) {
466 				auto res = plugin.createCollection(url, username);
467 				bindResourcePlugins(res);
468 
469 				notice(NoticeAction.created, res);
470 
471 				return res;
472 			}
473 
474 		throw new DavException(HTTPStatus.methodNotAllowed, "No plugin available.");
475 	}
476 
477 	DavResource createResource(URL url, string username) {
478 		foreach_reverse(plugin; plugins)
479 			if(plugin.canCreateResource(url, username)) {
480 				auto res = plugin.createResource(url, username);
481 				bindResourcePlugins(res);
482 
483 				notice(NoticeAction.created, res);
484 
485 				return res;
486 			}
487 
488 		throw new DavException(HTTPStatus.methodNotAllowed, "No plugin available.");
489 	}
490 
491 	void bindResourcePlugins(DavResource resource) {
492 		foreach(plugin; plugins) {
493 			plugin.bindResourcePlugins(resource);
494 		}
495 	}
496 
497 	bool exists(URL url, string username) {
498 		foreach_reverse(plugin; plugins) {
499 			if(plugin.exists(url, username))
500 				return true;
501 		}
502 
503 		return false;
504 	}
505 
506 	Path[] childList(URL url, string username) {
507 		bool[Path] childs;
508 
509 		foreach_reverse(plugin; plugins) {
510 			foreach(item; plugin.childList(url, username)) {
511 				childs[item] = true;
512 			}
513 		}
514 
515 		return childs.keys;
516 	}
517 
518 	bool canCreateCollection(URL url, string username) {
519 		foreach_reverse(plugin; plugins)
520 			if(plugin.canCreateCollection(url, username))
521 				return true;
522 
523 		return false;
524 	}
525 
526 	bool canCreateResource(URL url, string username) {
527 		foreach_reverse(plugin; plugins)
528 			if(plugin.canCreateResource(url, username))
529 				return true;
530 
531 		return false;
532 	}
533 
534 	void options(DavRequest request, DavResponse response) {
535 		DavResource resource = getResource(request.url, request.username);
536 
537 		string path = request.path;
538 
539 		string[] support;
540 
541 		foreach_reverse(plugin; plugins)
542 			support ~= plugin.support(request.url, request.username);
543 
544 		auto allow = "OPTIONS, GET, HEAD, DELETE, PROPFIND, PUT, PROPPATCH, COPY, MOVE, LOCK, UNLOCK, REPORT";
545 
546 		response["Accept-Ranges"] = "bytes";
547 		response["DAV"] = uniq(support).join(",");
548 		response["Allow"] = allow;
549 		response["MS-Author-Via"] = "DAV";
550 
551 		response.flush;
552 	}
553 
554 	void propfind(DavRequest request, DavResponse response) {
555 		bool[string] requestedProperties = propList(request.content);
556 		DavResource[] list;
557 
558 		if(!exists(request.url, request.username))
559 			throw new DavException(HTTPStatus.notFound, "Resource does not exist.");
560 
561 		list = getResources(request.url, request.depth, request.username);
562 
563 		response.setPropContent(list, requestedProperties);
564 
565 		response.flush;
566 	}
567 
568 	void proppatch(DavRequest request, DavResponse response) {
569 		auto ifHeader = request.ifCondition;
570 		response.statusCode = HTTPStatus.ok;
571 
572 		DavStorage.locks.check(request.url, ifHeader);
573 		DavResource resource = getResource(request.url, request.username);
574 
575 		notice(NoticeAction.changed, resource);
576 
577 		auto xmlString = resource.propPatch(request.content);
578 
579 		response.content = xmlString;
580 		response.flush;
581 	}
582 
583 	void report(DavRequest request, DavResponse response) {
584 		auto ifHeader = request.ifCondition;
585 
586 		string report = "";
587 
588 		foreach_reverse(plugin; plugins) {
589 			if(plugin.hasReport(request.url, request.username, request.content.reportName)) {
590 				plugin.report(request, response);
591 				return;
592 			}
593 		}
594 
595 		throw new DavException(HTTPStatus.notFound, "There is no report.");
596 	}
597 
598 	void lock(DavRequest request, DavResponse response) {
599 		auto ifHeader = request.ifCondition;
600 
601 		DavLockInfo currentLock;
602 
603 		auto resource = getOrCreateResource(request.url, request.username, response.statusCode);
604 
605 		if(request.contentLength != 0) {
606 			currentLock = DavLockInfo.fromXML(request.content, resource);
607 
608 			if(currentLock.scopeLock == DavLockInfo.Scope.sharedLock && DavStorage.locks.hasExclusiveLock(resource.fullURL))
609 				throw new DavException(HTTPStatus.locked, "Already has an exclusive locked.");
610 			else if(currentLock.scopeLock == DavLockInfo.Scope.exclusiveLock && DavStorage.locks.hasLock(resource.fullURL))
611 				throw new DavException(HTTPStatus.locked, "Already locked.");
612 			else if(currentLock.scopeLock == DavLockInfo.Scope.exclusiveLock)
613 				DavStorage.locks.check(request.url, ifHeader);
614 
615 			DavStorage.locks.add(currentLock);
616 		} else if(request.contentLength == 0) {
617 			string uuid = ifHeader.getAttr("", resource.href);
618 
619 			auto tmpUrl = resource.url;
620 			while(currentLock is null) {
621 				currentLock = DavStorage.locks[tmpUrl.toString, uuid];
622 				tmpUrl = tmpUrl.parentURL;
623 			}
624 		} else if(ifHeader.isEmpty)
625 			throw new DavException(HTTPStatus.internalServerError, "LOCK body expected.");
626 
627 		if(currentLock is null)
628 			throw new DavException(HTTPStatus.internalServerError, "LOCK not created.");
629 
630 		currentLock.timeout = request.timeout;
631 
632 		response["Lock-Token"] = "<" ~ currentLock.uuid ~ ">";
633 		response.mimeType = "application/xml";
634 		response.content = `<?xml version="1.0" encoding="utf-8" ?><d:prop xmlns:d="DAV:"><d:lockdiscovery> ` ~ currentLock.toString ~ `</d:lockdiscovery></d:prop>`;
635 		response.flush;
636 	}
637 
638 	void unlock(DavRequest request, DavResponse response) {
639 		auto resource = getResource(request.url, request.username);
640 
641 		DavStorage.locks.remove(resource, request.lockToken);
642 
643 		response.statusCode = HTTPStatus.noContent;
644 		response.flush;
645 	}
646 
647 	void get(DavRequest request, DavResponse response) {
648 		DavResource resource = getResource(request.url, request.username);
649 
650 		response["Etag"] = "\"" ~ resource.eTag ~ "\"";
651 		response["Last-Modified"] = toRFC822DateTimeString(resource.lastModified);
652 		response["Content-Type"] = resource.contentType;
653 		response["Content-Length"] = resource.contentLength.to!string;
654 
655 		if(!request.ifModifiedSince(resource) || !request.ifNoneMatch(resource)) {
656 			response.statusCode = HTTPStatus.NotModified;
657 			response.flush;
658 			return;
659 		}
660 
661 		response.flush(resource);
662 		DavStorage.locks.setETag(resource.url, resource.eTag);
663 	}
664 
665 	void head(DavRequest request, DavResponse response) {
666 		DavResource resource = getResource(request.url, request.username);
667 
668 		response["Etag"] = "\"" ~ resource.eTag ~ "\"";
669 		response["Last-Modified"] = toRFC822DateTimeString(resource.lastModified);
670 		response["Content-Type"] = resource.contentType;
671 		response["Content-Length"] = resource.contentLength.to!string;
672 
673 		if(!request.ifModifiedSince(resource) || !request.ifNoneMatch(resource)) {
674 			response.statusCode = HTTPStatus.NotModified;
675 			response.flush;
676 			return;
677 		}
678 
679 		response.flush;
680 		DavStorage.locks.setETag(resource.url, resource.eTag);
681 	}
682 
683 	void put(DavRequest request, DavResponse response) {
684 		DavResource resource = getOrCreateResource(request.url, request.username, response.statusCode);
685 		DavStorage.locks.check(request.url, request.ifCondition);
686 
687 		resource.setContent(request.stream, request.contentLength);
688 		notice(NoticeAction.changed, resource);
689 
690 		DavStorage.locks.setETag(resource.url, resource.eTag);
691 
692 		response.statusCode = HTTPStatus.created;
693 
694 		response.flush;
695 	}
696 
697 	void mkcol(DavRequest request, DavResponse response) {
698 		auto ifHeader = request.ifCondition;
699 
700 		if(request.contentLength > 0)
701 			throw new DavException(HTTPStatus.unsupportedMediaType, "Body must be empty");
702 
703 		if(!exists(request.url.parentURL, request.username))
704 			throw new DavException(HTTPStatus.conflict, "Missing parent");
705 
706 		DavStorage.locks.check(request.url, ifHeader);
707 
708 		response.statusCode = HTTPStatus.created;
709 		createCollection(request.url, request.username);
710 		notice(NoticeAction.created, getResource(request.url, request.username));
711 		response.flush;
712 	}
713 
714 	void remove(DavRequest request, DavResponse response) {
715 		auto ifHeader = request.ifCondition;
716 		auto url = request.url;
717 
718 		response.statusCode = HTTPStatus.noContent;
719 
720 		if(url.anchor != "" || request.requestUrl.indexOf("#") != -1)
721 			throw new DavException(HTTPStatus.conflict, "Missing parent");
722 
723 		if(!exists(url, request.username))
724 			throw new DavException(HTTPStatus.notFound, "Not found.");
725 
726 		DavStorage.locks.check(url, ifHeader);
727 
728 		removeResource(url, request.username);
729 
730 		response.flush;
731 	}
732 
733 	void move(DavRequest request, DavResponse response) {
734 		auto ifHeader = request.ifCondition;
735 		auto resource = getResource(request.url, request.username);
736 
737 		DavStorage.locks.check(request.url, ifHeader);
738 		DavStorage.locks.check(request.destination, ifHeader);
739 
740 		copy(request, response);
741 		remove(request, response);
742 
743 		response.flush;
744 	}
745 
746 	void copy(DavRequest request, DavResponse response) {
747 		string username = request.username;
748 
749 		URL getDestinationUrl(DavResource source) const {
750 			auto path = request.url.path;
751 			auto sourcePath = source.url.path[path.length..$];
752 			auto destinationPath = request.destination.path ~ sourcePath;
753 
754 			auto url = source.url;
755 			url.path = destinationPath;
756 
757 			return url;
758 		}
759 
760 		void localCopy(DavResource source, DavResource destination) {
761 			if(source.isCollection) {
762 				auto list = getResources(request.url, DavDepth.infinity, username);
763 
764 				foreach(child; list) {
765 					auto destinationUrl = getDestinationUrl(child);
766 
767 					if(child.isCollection && !exists(destinationUrl, username)) {
768 						createCollection(getDestinationUrl(child), username);
769 					} else if(!child.isCollection) {
770 						HTTPStatus statusCode;
771 						DavResource destinationChild = getOrCreateResource(getDestinationUrl(child), username, statusCode);
772 						destinationChild.setContent(child.stream, child.contentLength);
773 					}
774 				}
775 			} else {
776 				destination.setContent(source.stream, source.contentLength);
777 			}
778 
779 			source.copyPropertiesTo(destination.url);
780 		}
781 
782 		DavResource source = getResource(request.url, username);
783 		DavResource destination;
784 		HTTPStatus destinationStatus;
785 
786 		DavStorage.locks.check(request.destination, request.ifCondition);
787 
788 		if(!exists(request.destination.parentURL, username))
789 			throw new DavException(HTTPStatus.conflict, "Conflict. `" ~ request.destination.parentURL.toString ~ "` does not exist.");
790 
791 		if(!request.overwrite && exists(request.destination, username))
792 			throw new DavException(HTTPStatus.preconditionFailed, "Destination already exists.");
793 
794 		if(exists(request.destination, username))
795 			destination = getResource(request.destination, username);
796 
797 		response.statusCode = HTTPStatus.created;
798 
799 		URL destinationUrl = request.destination;
800 
801 		if(destination !is null && destination.isCollection && !source.isCollection) {
802 			destinationUrl.path = destinationUrl.path ~ source.url.path.head;
803 			destination = null;
804 			response.statusCode = HTTPStatus.noContent;
805 		}
806 
807 		if(destination !is null && !destination.isCollection && source.isCollection) {
808 			removeResource(destinationUrl, username);
809 		}
810 
811 		if(destination is null) {
812 			if(source.isCollection)
813 				destination = createCollection(destinationUrl, username);
814 			else
815 				destination = createResource(destinationUrl, username);
816 		}
817 
818 		localCopy(source, destination);
819 		notice(NoticeAction.changed, destination);
820 
821 		response.flush;
822 	}
823 
824 	void notice(NoticeAction action, DavResource resource) {
825 		foreach_reverse(plugin; plugins)
826 			plugin.notice(action, resource);
827 	}
828 }
829 
830 /// Hook vibe.d requests to the right DAV method
831 HTTPServerRequestDelegate serveDav(T : IDav)(T dav) {
832 	void callback(HTTPServerRequest req, HTTPServerResponse res)
833 	{
834 		try {
835 			debug {
836 				writeln("\n\n\n");
837 
838 				writeln("==========================================================");
839 				writeln(req.fullURL);
840 				writeln("Method: ", req.method, "\n");
841 
842 				foreach(key, val; req.headers)
843 					writeln(key, ": ", val);
844 			}
845 
846 			writeln(1);
847 			DavRequest request = DavRequest(req);
848 			DavResponse response = DavResponse(res);
849 
850 			writeln(2);
851 			if(req.method == HTTPMethod.OPTIONS) {
852 				dav.options(request, response);
853 			} else if(req.method == HTTPMethod.PROPFIND) {
854 				dav.propfind(request, response);
855 			} else if(req.method == HTTPMethod.HEAD) {
856 				dav.head(request, response);
857 			} else if(req.method == HTTPMethod.GET) {
858 				dav.get(request, response);
859 			} else if(req.method == HTTPMethod.PUT) {
860 				dav.put(request, response);
861 			} else if(req.method == HTTPMethod.PROPPATCH) {
862 				dav.proppatch(request, response);
863 			} else if(req.method == HTTPMethod.LOCK) {
864 				dav.lock(request, response);
865 			} else if(req.method == HTTPMethod.UNLOCK) {
866 				dav.unlock(request, response);
867 			} else if(req.method == HTTPMethod.MKCOL) {
868 				dav.mkcol(request, response);
869 			} else if(req.method == HTTPMethod.DELETE) {
870 				dav.remove(request, response);
871 			} else if(req.method == HTTPMethod.COPY) {
872 				dav.copy(request, response);
873 			} else if(req.method == HTTPMethod.MOVE) {
874 				dav.move(request, response);
875 			} else if(req.method == HTTPMethod.REPORT) {
876 				dav.report(request, response);
877 			} else {
878 				res.statusCode = HTTPStatus.notImplemented;
879 				res.writeBody("", "text/plain");
880 			}
881 
882 
883 			writeln(3);
884 		} catch(DavException e) {
885 			writeln(4);
886 			writeln("ERROR:",e.status.to!int, "(", e.status, ") - ", e.msg);
887 
888 			res.statusCode = e.status;
889 			res.writeBody(e.msg, e.mime);
890 		} catch(Throwable t) {
891 
892 			writeln(5);
893 			writeln("ERROR:",t);
894 			res.statusCode = 500;
895 			res.writeBody(t.to!string);
896 		}
897 
898 		writeln(6);
899 		debug {
900 			writeln("\nSENT:", res.statusCode.to!int, "(", res.statusCode, ")");
901 		}
902 
903 		writeln(7);
904 	}
905 
906 	return &callback;
907 }