Merge lp:~vila/bzr/webdav-in-core into lp:bzr

Proposed by Vincent Ladeuil
Status: Work in progress
Proposed branch: lp:~vila/bzr/webdav-in-core
Merge into: lp:bzr
Diff against target: 1896 lines (+1837/-1)
9 files modified
bzrlib/plugins/webdav/NOTES (+27/-0)
bzrlib/plugins/webdav/TODO (+48/-0)
bzrlib/plugins/webdav/__init__.py (+47/-0)
bzrlib/plugins/webdav/tests/__init__.py (+23/-0)
bzrlib/plugins/webdav/tests/dav_server.py (+452/-0)
bzrlib/plugins/webdav/tests/test_webdav.py (+359/-0)
bzrlib/plugins/webdav/webdav.py (+873/-0)
doc/en/release-notes/bzr-2.6.txt (+2/-0)
doc/en/whats-new/whats-new-in-2.6.txt (+6/-1)
To merge this branch: bzr merge lp:~vila/bzr/webdav-in-core
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) Needs Information
Review via email: mp+99371@code.launchpad.net

Description of the change

This merges the bzr-webdav plugin into core.

This plugin has been quite stable for years but was broken by a recent
change in bzr (related to the ssl cert verification). Bringing it into core
is one way to address potential issues like this and is mostly an experiment
to see what happen when a plugin is merged into core.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

In general, I like the idea of shipping the webdav plugin.

Wouldn't it make sense to merge some (all?) of this code into the HTTP implementation? I don't see a particular reason why WebDAV support couldn't be in the default HTTP implementation. At the very least it will mean people no longer see a mkdir() not implemented error when they try to write over HTTP but instead a "remote server doesn't support MKDIR" error.

That said, I don't think this integration should be a blocker for merging the webdav plugin into core. Let's move it one step at a time. :-)

Revision history for this message
Vincent Ladeuil (vila) wrote :

> In general, I like the idea of shipping the webdav plugin.
>
> Wouldn't it make sense to merge some (all?) of this code into the HTTP
> implementation? I don't see a particular reason why WebDAV support couldn't be
> in the default HTTP implementation.

'default' is the key word here, the very webdav implementation was against pycurl but it couldn't be completed (if my memory serves me well).

> At the very least it will mean people no
> longer see a mkdir() not implemented error when they try to write over HTTP
> but instead a "remote server doesn't support MKDIR" error.

Goog point, worth filing a bug if we agree to land this.

> That said, I don't think this integration should be a blocker for merging
> the webdav plugin into core. Let's move it one step at a time. :-)

Cool, I'm not sure how to interpret this a vote though ;)

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

> > In general, I like the idea of shipping the webdav plugin.
> >
> > Wouldn't it make sense to merge some (all?) of this code into the HTTP
> > implementation? I don't see a particular reason why WebDAV support couldn't
> be
> > in the default HTTP implementation.
>
> 'default' is the key word here, the very webdav implementation was against
> pycurl but it couldn't be completed (if my memory serves me well).
Speaking of which - this code seems to be based on urllib, but it still uses raise_curl_error ?

> > That said, I don't think this integration should be a blocker for merging
> > the webdav plugin into core. Let's move it one step at a time. :-)
> Cool, I'm not sure how to interpret this a vote though ;)
I haven't done a thorough review of the code yet, was mostly saying that I agree with merging this in principle.

Revision history for this message
Vincent Ladeuil (vila) wrote :

> > 'default' is the key word here, the very webdav implementation was against
> > pycurl but it couldn't be completed (if my memory serves me well).
> Speaking of which - this code seems to be based on urllib, but it still uses
> raise_curl_error ?

Bah, who mentioned coverage as a criteria to accept plugins in core ? :-}

>
> > > That said, I don't think this integration should be a blocker for merging
> > > the webdav plugin into core. Let's move it one step at a time. :-)
> > Cool, I'm not sure how to interpret this a vote though ;)
> I haven't done a thorough review of the code yet, was mostly saying that I
> agree with merging this in principle.

Ok, thanks.

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

I've had a closer look at the code now. It looks fairly reasonable, though there aren't a lot of Transport-level tests (is that done by bzrlib.tests.per_transport ?).

It's a bit surprising to see _raise_curl_http_error despite the backend being based on the urllib HTTP implementation. Is that a bug?

review: Needs Information
Revision history for this message
Vincent Ladeuil (vila) wrote :

> I've had a closer look at the code now. It looks fairly reasonable, though
> there aren't a lot of Transport-level tests (is that done by
> bzrlib.tests.per_transport ?).

Yes, most of the testing comes from that, only webdav-specific parts are in the plugin.

>
> It's a bit surprising to see _raise_curl_http_error despite the backend being
> based on the urllib HTTP implementation. Is that a bug?

Well, more probably dead code.

If I'm not mistaken, these calls can only be triggered if an unexpected error code is returned which is *already* handled via accepted_errors=[...] in the requests definitions.

I've started looking at the coverage and will use this work to validate the above. Putting to wip until then.

Unmerged revisions

6514. By Vincent Ladeuil

Fix failing tests and remove the now useless info.py.

6513. By Vincent Ladeuil

Remove version compatibility code, cleanup some bits

6512. By Vincent Ladeuil

Merge the webdav plugin after a double join

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'bzrlib/plugins/webdav'
2=== added file 'bzrlib/plugins/webdav/NOTES'
3--- bzrlib/plugins/webdav/NOTES 1970-01-01 00:00:00 +0000
4+++ bzrlib/plugins/webdav/NOTES 2012-03-26 16:57:22 +0000
5@@ -0,0 +1,27 @@
6+Installation example:
7+
8+<IfModule mod_dav.c>
9+Alias /bzr /srv/DAV
10+<Directory /srv/DAV>
11+ DAV On
12+ # DirectorySlash tells apache to reply with redirections if
13+ # directories miss their final '/'. It does not play well with
14+ # bzr (to they the least) and provide no benefits in our
15+ # case. So just turn it off.
16+ DirectorySlash Off
17+ # We need to activate the following which is off by
18+ # default. For good security reasons which don't apply to
19+ # bzr directories ;)
20+ DavDepthInfinity on
21+ # The simplest auth scheme is basic, just given as an
22+ # example, using https is recommanded with it, or at
23+ # least digest if https is not possible.
24+ AuthType Basic
25+ AuthName bzr
26+ AuthUserFile /etc/apache2/dav.users
27+ <LimitExcept GET OPTIONS>
28+ # Write access requires authentication
29+ Require valid-user
30+ </LimitExcept>
31+</Directory>
32+</IfModule>
33
34=== added file 'bzrlib/plugins/webdav/TODO'
35--- bzrlib/plugins/webdav/TODO 1970-01-01 00:00:00 +0000
36+++ bzrlib/plugins/webdav/TODO 2012-03-26 16:57:22 +0000
37@@ -0,0 +1,48 @@
38+* webdav.py
39+
40+** We can detect that the server do not accept "write" operations
41+ (it will return 501) and raise InvalidHttpRequest(to be
42+ defined as a daughter of InvalidHttpResponse) but what will
43+ the upper layers do ?
44+
45+** 20060908 All *_file functions are defined in terms of *_bytes
46+ because we have to read the file to create a proper PUT
47+ request. Is it possible to define PUT with a file-like
48+ object, so that we don't have to potentially read in and hold
49+ onto potentially 600MB of file contents?
50+
51+** Factor out the error handling. Try to use
52+ Transport.translate_error if it becomes an accessible
53+ function. Otherwise duplicate it here (bad)
54+
55+* tests
56+
57+** Implement the testing of the range header for PUT requests
58+ (GET request are already heavily tested in bzr). Test servers
59+ are available there too. This will also help for reporting
60+ bugs against lighttp.
61+
62+** Turning directory indexes off may make the server reports that
63+ an existing directory does not exist. Reportedly, using
64+ multiviews can provoke that too. Investigate and fix.
65+
66+** A DAV web server can't handle mode on files because:
67+
68+ - there is nothing in the protocol for that (bar some of them
69+ via PROPPATCH, but only for apache2 anyway),
70+
71+ - the server itself generally uses the mode for its own
72+ purposes, except if you make it run under suid which is really,
73+ really dangerous (Apache should be compiled with
74+ -DBIG_SECURITY_HOLE for those who didn't get the message).
75+
76+ That means this transport will do no better. May be the file
77+ mode should be a file property handled explicitely inside the
78+ repositories and applied by bzr in the working trees. That
79+ implies a mean to store file properties, apply them, detecting
80+ their changes, etc.
81+
82+ It may be possible to use PROPPATCH to handle mode bits, but
83+ bzr doesn't try to handle remote working trees. So until the
84+ neeed arises, this will remain as is.
85+
86
87=== added file 'bzrlib/plugins/webdav/__init__.py'
88--- bzrlib/plugins/webdav/__init__.py 1970-01-01 00:00:00 +0000
89+++ bzrlib/plugins/webdav/__init__.py 2012-03-26 16:57:22 +0000
90@@ -0,0 +1,47 @@
91+# Copyright (C) 2006-2009, 2011, 2012 Canonical Ltd
92+#
93+# This program is free software; you can redistribute it and/or modify
94+# it under the terms of the GNU General Public License as published by
95+# the Free Software Foundation; either version 2 of the License, or
96+# (at your option) any later version.
97+#
98+# This program is distributed in the hope that it will be useful,
99+# but WITHOUT ANY WARRANTY; without even the implied warranty of
100+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
101+# GNU General Public License for more details.
102+#
103+# You should have received a copy of the GNU General Public License
104+# along with this program; if not, write to the Free Software
105+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
106+
107+from __future__ import absolute_import
108+
109+__doc__ = """An http transport, using webdav to allow pushing.
110+
111+This defines the HttpWebDAV transport, which implement the necessary
112+handling of WebDAV to allow pushing on an http server.
113+"""
114+
115+import bzrlib
116+# Since we are a built-in plugin we share the bzrlib version
117+from bzrlib import (
118+ transport,
119+ version_info,
120+ )
121+
122+transport.register_urlparse_netloc_protocol('http+webdav')
123+transport.register_urlparse_netloc_protocol('https+webdav')
124+
125+transport.register_lazy_transport(
126+ 'https+webdav://', 'bzrlib.plugins.webdav.webdav', 'HttpDavTransport')
127+transport.register_lazy_transport(
128+ 'http+webdav://', 'bzrlib.plugins.webdav.webdav', 'HttpDavTransport')
129+
130+
131+def load_tests(basic_tests, module, loader):
132+ testmod_names = [
133+ 'tests',
134+ ]
135+ basic_tests.addTest(loader.loadTestsFromModuleNames(
136+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
137+ return basic_tests
138
139=== added directory 'bzrlib/plugins/webdav/tests'
140=== added file 'bzrlib/plugins/webdav/tests/__init__.py'
141--- bzrlib/plugins/webdav/tests/__init__.py 1970-01-01 00:00:00 +0000
142+++ bzrlib/plugins/webdav/tests/__init__.py 2012-03-26 16:57:22 +0000
143@@ -0,0 +1,23 @@
144+# Copyright (C) 2008 by Canonical Ltd
145+#
146+# This program is free software; you can redistribute it and/or modify
147+# it under the terms of the GNU General Public License as published by
148+# the Free Software Foundation; either version 2 of the License, or
149+# (at your option) any later version.
150+#
151+# This program is distributed in the hope that it will be useful,
152+# but WITHOUT ANY WARRANTY; without even the implied warranty of
153+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
154+# GNU General Public License for more details.
155+#
156+# You should have received a copy of the GNU General Public License
157+# along with this program; if not, write to the Free Software
158+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
159+
160+def load_tests(basic_tests, module, loader):
161+ testmod_names = [
162+ 'test_webdav',
163+ ]
164+ basic_tests.addTest(loader.loadTestsFromModuleNames(
165+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
166+ return basic_tests
167
168=== added file 'bzrlib/plugins/webdav/tests/dav_server.py'
169--- bzrlib/plugins/webdav/tests/dav_server.py 1970-01-01 00:00:00 +0000
170+++ bzrlib/plugins/webdav/tests/dav_server.py 2012-03-26 16:57:22 +0000
171@@ -0,0 +1,452 @@
172+# Copyright (C) 2008, 2009, 2011 Canonical Ltd
173+#
174+# This program is free software; you can redistribute it and/or modify
175+# it under the terms of the GNU General Public License as published by
176+# the Free Software Foundation; either version 2 of the License, or
177+# (at your option) any later version.
178+#
179+# This program is distributed in the hope that it will be useful,
180+# but WITHOUT ANY WARRANTY; without even the implied warranty of
181+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
182+# GNU General Public License for more details.
183+#
184+# You should have received a copy of the GNU General Public License
185+# along with this program; if not, write to the Free Software
186+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
187+
188+"""DAV test server.
189+
190+This defines the TestingDAVRequestHandler and the DAVServer classes which
191+implements the DAV specification parts used by the webdav plugin.
192+"""
193+
194+
195+import errno
196+import os
197+import os.path # FIXME: Can't we use bzrlib.osutils ?
198+import re
199+import shutil # FIXME: Can't we use bzrlib.osutils ?
200+import stat
201+import time
202+import urlparse # FIXME: Can't we use bzrlib.urlutils ?
203+
204+
205+from bzrlib import (
206+ tests,
207+ trace,
208+ urlutils,
209+ )
210+from bzrlib.tests import http_server
211+
212+
213+class TestingDAVRequestHandler(http_server.TestingHTTPRequestHandler):
214+ """
215+ Subclass of TestingHTTPRequestHandler handling DAV requests.
216+
217+ This is not a full implementation of a DAV server, only the parts
218+ really used by the plugin are.
219+ """
220+
221+ _RANGE_HEADER_RE = re.compile(
222+ r'bytes (?P<begin>\d+)-(?P<end>\d+)/(?P<size>\d+|\*)')
223+
224+
225+ def date_time_string(self, timestamp=None):
226+ """Return the current date and time formatted for a message header."""
227+ if timestamp is None:
228+ timestamp = time.time()
229+ year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
230+ s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
231+ self.weekdayname[wd],
232+ day, self.monthname[month], year,
233+ hh, mm, ss)
234+ return s
235+
236+ def _read(self, length):
237+ """Read the client socket"""
238+ return self.rfile.read(length)
239+
240+ def _readline(self):
241+ """Read a full line on the client socket"""
242+ return self.rfile.readline()
243+
244+ def read_body(self):
245+ """Read the body either by chunk or as a whole."""
246+ content_length = self.headers.get('Content-Length')
247+ encoding = self.headers.get('Transfer-Encoding')
248+ if encoding is not None:
249+ assert encoding == 'chunked'
250+ body = []
251+ # We receive the content by chunk
252+ while True:
253+ length, data = self.read_chunk()
254+ if length == 0:
255+ break
256+ body.append(data)
257+ body = ''.join(body)
258+
259+ else:
260+ if content_length is not None:
261+ body = self._read(int(content_length))
262+
263+ return body
264+
265+ def read_chunk(self):
266+ """Read a chunk of data.
267+
268+ A chunk consists of:
269+ - a line containing the length of the data in hexa,
270+ - the data.
271+ - a empty line.
272+
273+ An empty chunk specifies a length of zero
274+ """
275+ length = int(self._readline(),16)
276+ data = None
277+ if length != 0:
278+ data = self._read(length)
279+ # Eats the newline following the chunk
280+ self._readline()
281+ return length, data
282+
283+ def send_head(self):
284+ """Specialized version of SimpleHttpServer.
285+
286+ We *don't* want the apache behavior of permanently redirecting
287+ directories without trailing slashes to directories with trailing
288+ slashes. That's a waste and a severe penalty for clients with high
289+ latency.
290+
291+ The installation documentation of the plugin should mention the
292+ DirectorySlash apache directive and insists on turning it *Off*.
293+ """
294+ path = self.translate_path(self.path)
295+ f = None
296+ if os.path.isdir(path):
297+ for index in "index.html", "index.htm":
298+ index = os.path.join(path, index)
299+ if os.path.exists(index):
300+ path = index
301+ break
302+ else:
303+ return self.list_directory(path)
304+ ctype = self.guess_type(path)
305+ if ctype.startswith('text/'):
306+ mode = 'r'
307+ else:
308+ mode = 'rb'
309+ try:
310+ f = open(path, mode)
311+ except IOError:
312+ self.send_error(404, "File not found")
313+ return None
314+ self.send_response(200)
315+ self.send_header("Content-type", ctype)
316+ fs = os.fstat(f.fileno())
317+ self.send_header("Content-Length", str(fs[6]))
318+ self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
319+ self.end_headers()
320+ return f
321+
322+ def do_PUT(self):
323+ """Serve a PUT request."""
324+ # FIXME: test_put_file_unicode makes us emit a traceback because a
325+ # UnicodeEncodeError occurs after the request headers have been sent
326+ # but before the body can be send. It's harmless and does not make the
327+ # test fails. Adressing that will mean protecting all reads from the
328+ # socket, which is too heavy for now -- vila 20070917
329+ path = self.translate_path(self.path)
330+ trace.mutter("do_PUT rel: [%s], abs: [%s]" % (self.path, path))
331+
332+ do_append = False
333+ # Check the Content-Range header
334+ range_header = self.headers.get('Content-Range')
335+ if range_header is not None:
336+ match = self._RANGE_HEADER_RE.match(range_header)
337+ if match is None:
338+ # FIXME: RFC2616 says to return a 501 if we don't
339+ # understand the Content-Range header, but Apache
340+ # just ignores them (bad Apache).
341+ self.send_error(501, 'Not Implemented')
342+ return
343+ begin = int(match.group('begin'))
344+ do_append = True
345+
346+ if self.headers.get('Expect') == '100-continue':
347+ # Tell the client to go ahead, we're ready to get the content
348+ self.send_response(100,"Continue")
349+ self.end_headers()
350+
351+ try:
352+ trace.mutter("do_PUT will try to open: [%s]" % path)
353+ # Always write in binary mode.
354+ if do_append:
355+ f = open(path,'ab')
356+ f.seek(begin)
357+ else:
358+ f = open(path, 'wb')
359+ except (IOError, OSError), e :
360+ trace.mutter("do_PUT got: [%r] while opening/seeking on [%s]"
361+ % (e, self.path))
362+ self.send_error(409, 'Conflict')
363+ return
364+
365+ try:
366+ data = self.read_body()
367+ f.write(data)
368+ except (IOError, OSError):
369+ # FIXME: We leave a partially written file here
370+ self.send_error(409, "Conflict")
371+ f.close()
372+ return
373+ f.close()
374+ trace.mutter("do_PUT done: [%s]" % self.path)
375+ self.send_response(201)
376+ self.end_headers()
377+
378+ def do_MKCOL(self):
379+ """
380+ Serve a MKCOL request.
381+
382+ MKCOL is an mkdir in DAV terminology for our part.
383+ """
384+ path = self.translate_path(self.path)
385+ trace.mutter("do_MKCOL rel: [%s], abs: [%s]" % (self.path,path))
386+ try:
387+ os.mkdir(path)
388+ except (IOError, OSError),e:
389+ if e.errno in (errno.ENOENT, ):
390+ self.send_error(409, "Conflict")
391+ elif e.errno in (errno.EEXIST, errno.ENOTDIR):
392+ self.send_error(405, "Not allowed")
393+ else:
394+ # Ok we fail for an unnkown reason :-/
395+ raise
396+ else:
397+ self.send_response(201)
398+ self.end_headers()
399+
400+ def do_COPY(self):
401+ """Serve a COPY request."""
402+
403+ url_to = self.headers.get('Destination')
404+ if url_to is None:
405+ self.send_error(400,"Destination header missing")
406+ return
407+ (scheme, netloc, rel_to,
408+ params, query, fragment) = urlparse.urlparse(url_to)
409+ trace.mutter("urlparse: (%s) [%s]" % (url_to, rel_to))
410+ trace.mutter("do_COPY rel_from: [%s], rel_to: [%s]" % (self.path,
411+ rel_to))
412+ abs_from = self.translate_path(self.path)
413+ abs_to = self.translate_path(rel_to)
414+ try:
415+ # TODO: Check that rel_from exists and rel_to does
416+ # not. In the mean time, just go along and trap
417+ # exceptions
418+ shutil.copyfile(abs_from,abs_to)
419+ except (IOError, OSError), e:
420+ if e.errno == errno.ENOENT:
421+ self.send_error(404,"File not found") ;
422+ else:
423+ self.send_error(409,"Conflict") ;
424+ else:
425+ # TODO: We may be able to return 204 "No content" if
426+ # rel_to was existing (even if the "No content" part
427+ # seems misleading, RFC2518 says so, stop arguing :)
428+ self.send_response(201)
429+ self.end_headers()
430+
431+ def do_DELETE(self):
432+ """Serve a DELETE request.
433+
434+ We don't implement a true DELETE as DAV defines it
435+ because we *should* fail to delete a non empty dir.
436+ """
437+ path = self.translate_path(self.path)
438+ trace.mutter("do_DELETE rel: [%s], abs: [%s]" % (self.path, path))
439+ try:
440+ # DAV makes no distinction between files and dirs
441+ # when required to nuke them, but we have to. And we
442+ # also watch out for symlinks.
443+ real_path = os.path.realpath(path)
444+ if os.path.isdir(real_path):
445+ os.rmdir(path)
446+ else:
447+ os.remove(path)
448+ except (IOError, OSError),e:
449+ if e.errno in (errno.ENOENT, ):
450+ self.send_error(404, "File not found")
451+ else:
452+ # Ok we fail for an unnkown reason :-/
453+ raise
454+ else:
455+ self.send_response(204) # Default success code
456+ self.end_headers()
457+
458+ def do_MOVE(self):
459+ """Serve a MOVE request."""
460+
461+ url_to = self.headers.get('Destination')
462+ if url_to is None:
463+ self.send_error(400, "Destination header missing")
464+ return
465+ overwrite_header = self.headers.get('Overwrite')
466+ if overwrite_header == 'F':
467+ should_overwrite = False
468+ else:
469+ should_overwrite = True
470+ (scheme, netloc, rel_to,
471+ params, query, fragment) = urlparse.urlparse(url_to)
472+ trace.mutter("urlparse: (%s) [%s]" % (url_to, rel_to))
473+ trace.mutter("do_MOVE rel_from: [%s], rel_to: [%s]" % (self.path,
474+ rel_to))
475+ abs_from = self.translate_path(self.path)
476+ abs_to = self.translate_path(rel_to)
477+ if should_overwrite is False and os.access(abs_to, os.F_OK):
478+ self.send_error(412, "Precondition Failed")
479+ return
480+ try:
481+ os.rename(abs_from, abs_to)
482+ except (IOError, OSError), e:
483+ if e.errno == errno.ENOENT:
484+ self.send_error(404, "File not found") ;
485+ else:
486+ self.send_error(409, "Conflict") ;
487+ else:
488+ # TODO: We may be able to return 204 "No content" if
489+ # rel_to was existing (even if the "No content" part
490+ # seems misleading, RFC2518 says so, stop arguing :)
491+ self.send_response(201)
492+ self.end_headers()
493+
494+ def _generate_response(self, path):
495+ local_path = self.translate_path(path)
496+ st = os.stat(local_path)
497+ prop = dict()
498+
499+ def _prop(ns, name, value=None):
500+ if value is None:
501+ return '<%s:%s/>' % (ns, name)
502+ else:
503+ return '<%s:%s>%s</%s:%s>' % (ns, name, value, ns, name)
504+
505+ # For namespaces (and test purposes), where apache2 use:
506+ # - lp1, we use liveprop,
507+ # - lp2, we use bzr
508+ if stat.S_ISDIR(st.st_mode):
509+ dpath = path
510+ if not dpath.endswith('/'):
511+ dpath += '/'
512+ prop['href'] = _prop('D', 'href', dpath)
513+ prop['type'] = _prop('liveprop', 'resourcetype', '<D:collection/>')
514+ prop['length'] = ''
515+ prop['exec'] = ''
516+ else:
517+ # FIXME: assert S_ISREG ? Handle symlinks ?
518+ prop['href'] = _prop('D', 'href', path)
519+ prop['type'] = _prop('liveprop', 'resourcetype')
520+ prop['length'] = _prop('liveprop', 'getcontentlength',
521+ st.st_size)
522+ if st.st_mode & stat.S_IXUSR:
523+ is_exec = 'T'
524+ else:
525+ is_exec = 'F'
526+ prop['exec'] = _prop('bzr', 'executable', is_exec)
527+ prop['status'] = _prop('D', 'status', 'HTTP/1.1 200 OK')
528+
529+ response = """<D:response xmlns:liveprop="DAV:" xmlns:bzr="DAV:">
530+ %(href)s
531+ <D:propstat>
532+ <D:prop>
533+ %(type)s
534+ %(length)s
535+ %(exec)s
536+ </D:prop>
537+ %(status)s
538+ </D:propstat>
539+</D:response>
540+""" % prop
541+ return response, st
542+
543+ def _generate_dir_responses(self, path, depth):
544+ local_path = self.translate_path(path)
545+ entries = os.listdir(local_path)
546+
547+ for entry in entries:
548+ entry_path = urlutils.escape(entry)
549+ if path.endswith('/'):
550+ entry_path = path + entry_path
551+ else:
552+ entry_path = path + '/' + entry_path
553+ response, st = self._generate_response(entry_path)
554+ yield response
555+ if depth == 'Infinity' and stat.S_ISDIR(st.st_mode):
556+ for sub_resp in self._generate_dir_responses(entry_path, depth):
557+ yield sub_resp
558+
559+ def do_PROPFIND(self):
560+ """Serve a PROPFIND request."""
561+ depth = self.headers.get('Depth')
562+ if depth is None:
563+ depth = 'Infinity'
564+ if depth not in ('0', '1', 'Infinity'):
565+ self.send_error(400, "Bad Depth")
566+ return
567+
568+ path = self.translate_path(self.path)
569+ # Don't bother parsing the body, we handle only allprop anyway.
570+ # FIXME: Handle the body :)
571+ data = self.read_body()
572+
573+ try:
574+ response, st = self._generate_response(self.path)
575+ except OSError, e:
576+ if e.errno == errno.ENOENT:
577+ self.send_error(404)
578+ return
579+ else:
580+ raise
581+
582+ if depth in ('1', 'Infinity') and stat.S_ISDIR(st.st_mode):
583+ dir_responses = self._generate_dir_responses(self.path, depth)
584+ else:
585+ dir_responses = []
586+
587+ # Generate the response, we don't care about performance, so we just
588+ # expand everything into a big string.
589+ response = """<?xml version="1.0" encoding="utf-8"?>
590+<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">
591+%s%s
592+</D:multistatus>""" % (response, ''.join(list(dir_responses)))
593+
594+ self.send_response(207)
595+ self.send_header('Content-length', len(response))
596+ self.end_headers()
597+ self.wfile.write(response)
598+
599+class DAVServer(http_server.HttpServer):
600+ """Subclass of HttpServer that gives http+webdav urls.
601+
602+ This is for use in testing: connections to this server will always go
603+ through _urllib where possible.
604+ """
605+
606+ def __init__(self):
607+ # We have special requests to handle that
608+ # HttpServer_urllib doesn't know about
609+ super(DAVServer,self).__init__(TestingDAVRequestHandler)
610+
611+ # urls returned by this server should require the webdav client impl
612+ _url_protocol = 'http+webdav'
613+
614+
615+class TestCaseWithDAVServer(tests.TestCaseWithTransport):
616+ """A support class that provides urls that are http+webdav://.
617+
618+ This is done by forcing the server to be an http DAV one.
619+ """
620+ def setUp(self):
621+ super(TestCaseWithDAVServer, self).setUp()
622+ self.transport_server = DAVServer
623+
624
625=== added file 'bzrlib/plugins/webdav/tests/test_webdav.py'
626--- bzrlib/plugins/webdav/tests/test_webdav.py 1970-01-01 00:00:00 +0000
627+++ bzrlib/plugins/webdav/tests/test_webdav.py 2012-03-26 16:57:22 +0000
628@@ -0,0 +1,359 @@
629+# Copyright (C) 2008 Canonical Ltd
630+#
631+# This program is free software; you can redistribute it and/or modify
632+# it under the terms of the GNU General Public License as published by
633+# the Free Software Foundation; either version 2 of the License, or
634+# (at your option) any later version.
635+#
636+# This program is distributed in the hope that it will be useful,
637+# but WITHOUT ANY WARRANTY; without even the implied warranty of
638+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
639+# GNU General Public License for more details.
640+#
641+# You should have received a copy of the GNU General Public License
642+# along with this program; if not, write to the Free Software
643+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
644+
645+"""Tests for the wedav plugin."""
646+
647+from cStringIO import StringIO
648+import stat
649+
650+
651+from bzrlib import (
652+ errors,
653+ tests,
654+ )
655+from bzrlib.plugins.webdav import webdav
656+
657+
658+def _get_list_dir_apache2_depth_1_prop():
659+ return """<?xml version="1.0" encoding="utf-8"?>
660+<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">
661+ <D:response>
662+ <D:href>/19016477731212686926.835527/</D:href>
663+ <D:propstat>
664+ <D:prop>
665+ </D:prop>
666+ <D:status>HTTP/1.1 200 OK</D:status>
667+ </D:propstat>
668+ </D:response>
669+ <D:response>
670+ <D:href>/19016477731212686926.835527/a</D:href>
671+ <D:propstat>
672+ <D:prop>
673+ </D:prop>
674+ <D:status>HTTP/1.1 200 OK</D:status>
675+ </D:propstat>
676+ </D:response>
677+ <D:response>
678+ <D:href>/19016477731212686926.835527/b</D:href>
679+ <D:propstat>
680+ <D:prop>
681+ </D:prop>
682+ <D:status>HTTP/1.1 200 OK</D:status>
683+ </D:propstat>
684+ </D:response>
685+ <D:response>
686+ <D:href>/19016477731212686926.835527/c/</D:href>
687+ <D:propstat>
688+ <D:prop>
689+ </D:prop>
690+ <D:status>HTTP/1.1 200 OK</D:status>
691+ </D:propstat>
692+ </D:response>
693+</D:multistatus>"""
694+
695+
696+def _get_list_dir_apache2_depth_1_allprop():
697+ return """<?xml version="1.0" encoding="utf-8"?>
698+<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">
699+ <D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
700+ <D:href>/</D:href>
701+ <D:propstat>
702+ <D:prop>
703+ <lp1:resourcetype><D:collection/></lp1:resourcetype>
704+ <lp1:creationdate>2008-06-08T10:50:38Z</lp1:creationdate>
705+ <lp1:getlastmodified>Sun, 08 Jun 2008 10:50:38 GMT</lp1:getlastmodified>
706+ <lp1:getetag>"da7f5a-cc-7722db80"</lp1:getetag>
707+ <D:supportedlock>
708+ <D:lockentry>
709+ <D:lockscope><D:exclusive/></D:lockscope>
710+ <D:locktype><D:write/></D:locktype>
711+ </D:lockentry>
712+ <D:lockentry>
713+ <D:lockscope><D:shared/></D:lockscope>
714+ <D:locktype><D:write/></D:locktype>
715+ </D:lockentry>
716+ </D:supportedlock>
717+ <D:lockdiscovery/>
718+ </D:prop>
719+ <D:status>HTTP/1.1 200 OK</D:status>
720+ </D:propstat>
721+ </D:response>
722+ <D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
723+ <D:href>/executable</D:href>
724+ <D:propstat>
725+ <D:prop>
726+ <lp1:resourcetype/>
727+ <lp1:creationdate>2008-06-08T09:50:15Z</lp1:creationdate>
728+ <lp1:getcontentlength>14</lp1:getcontentlength>
729+ <lp1:getlastmodified>Sun, 08 Jun 2008 09:50:11 GMT</lp1:getlastmodified>
730+ <lp1:getetag>"da9f81-0-9ef33ac0"</lp1:getetag>
731+ <lp2:executable>T</lp2:executable>
732+ <D:supportedlock>
733+ <D:lockentry>
734+ <D:lockscope><D:exclusive/></D:lockscope>
735+ <D:locktype><D:write/></D:locktype>
736+ </D:lockentry>
737+ <D:lockentry>
738+ <D:lockscope><D:shared/></D:lockscope>
739+ <D:locktype><D:write/></D:locktype>
740+ </D:lockentry>
741+ </D:supportedlock>
742+ <D:lockdiscovery/>
743+ </D:prop>
744+ <D:status>HTTP/1.1 200 OK</D:status>
745+ </D:propstat>
746+ </D:response>
747+ <D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
748+ <D:href>/read-only</D:href>
749+ <D:propstat>
750+ <D:prop>
751+ <lp1:resourcetype/>
752+ <lp1:creationdate>2008-06-08T09:50:11Z</lp1:creationdate>
753+ <lp1:getcontentlength>42</lp1:getcontentlength>
754+ <lp1:getlastmodified>Sun, 08 Jun 2008 09:50:11 GMT</lp1:getlastmodified>
755+ <lp1:getetag>"da9f80-0-9ef33ac0"</lp1:getetag>
756+ <lp2:executable>F</lp2:executable>
757+ <D:supportedlock>
758+ <D:lockentry>
759+ <D:lockscope><D:exclusive/></D:lockscope>
760+ <D:locktype><D:write/></D:locktype>
761+ </D:lockentry>
762+ <D:lockentry>
763+ <D:lockscope><D:shared/></D:lockscope>
764+ <D:locktype><D:write/></D:locktype>
765+ </D:lockentry>
766+ </D:supportedlock>
767+ <D:lockdiscovery/>
768+ </D:prop>
769+ <D:status>HTTP/1.1 200 OK</D:status>
770+ </D:propstat>
771+ </D:response>
772+ <D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
773+ <D:href>/titi</D:href>
774+ <D:propstat>
775+ <D:prop>
776+ <lp1:resourcetype/>
777+ <lp1:creationdate>2008-06-08T09:49:53Z</lp1:creationdate>
778+ <lp1:getcontentlength>6</lp1:getcontentlength>
779+ <lp1:getlastmodified>Sun, 08 Jun 2008 09:49:53 GMT</lp1:getlastmodified>
780+ <lp1:getetag>"da8cbc-6-9de09240"</lp1:getetag>
781+ <lp2:executable>F</lp2:executable>
782+ <D:supportedlock>
783+ <D:lockentry>
784+ <D:lockscope><D:exclusive/></D:lockscope>
785+ <D:locktype><D:write/></D:locktype>
786+ </D:lockentry>
787+ <D:lockentry>
788+ <D:lockscope><D:shared/></D:lockscope>
789+ <D:locktype><D:write/></D:locktype>
790+ </D:lockentry>
791+ </D:supportedlock>
792+ <D:lockdiscovery/>
793+ </D:prop>
794+ <D:status>HTTP/1.1 200 OK</D:status>
795+ </D:propstat>
796+ </D:response>
797+ <D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
798+ <D:href>/toto/</D:href>
799+ <D:propstat>
800+ <D:prop>
801+ <lp1:resourcetype><D:collection/></lp1:resourcetype>
802+ <lp1:creationdate>2008-06-06T08:07:07Z</lp1:creationdate>
803+ <lp1:getlastmodified>Fri, 06 Jun 2008 08:07:07 GMT</lp1:getlastmodified>
804+ <lp1:getetag>"da8cb9-44-f2ac20c0"</lp1:getetag>
805+ <D:supportedlock>
806+ <D:lockentry>
807+ <D:lockscope><D:exclusive/></D:lockscope>
808+ <D:locktype><D:write/></D:locktype>
809+ </D:lockentry>
810+ <D:lockentry>
811+ <D:lockscope><D:shared/></D:lockscope>
812+ <D:locktype><D:write/></D:locktype>
813+ </D:lockentry>
814+ </D:supportedlock>
815+ <D:lockdiscovery/>
816+ </D:prop>
817+ <D:status>HTTP/1.1 200 OK</D:status>
818+ </D:propstat>
819+ </D:response>
820+</D:multistatus>
821+"""
822+
823+
824+class TestDavSaxParser(tests.TestCase):
825+
826+ def _extract_dir_content_from_str(self, str):
827+ return webdav._extract_dir_content(
828+ 'http://localhost/blah', StringIO(str))
829+
830+ def _extract_stat_from_str(self, str):
831+ return webdav._extract_stat_info(
832+ 'http://localhost/blah', StringIO(str))
833+
834+ def test_unkown_format_response(self):
835+ # Valid but unrelated xml
836+ example = """<document/>"""
837+ self.assertRaises(errors.InvalidHttpResponse,
838+ self._extract_dir_content_from_str, example)
839+
840+ def test_list_dir_malformed_response(self):
841+ # Invalid xml, neither multistatus nor response are properly closed
842+ example = """<?xml version="1.0" encoding="utf-8"?>
843+<D:multistatus xmlns:D="DAV:" xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
844+<D:response>
845+<D:href>http://localhost/</D:href>"""
846+ self.assertRaises(errors.InvalidHttpResponse,
847+ self._extract_dir_content_from_str, example)
848+
849+ def test_list_dir_incomplete_format_response(self):
850+ # The information we need is not present
851+ example = """<?xml version="1.0" encoding="utf-8"?>
852+<D:multistatus xmlns:D="DAV:" xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
853+<D:response>
854+<D:href>http://localhost/</D:href>
855+</D:response>
856+<D:response>
857+<D:href>http://localhost/titi</D:href>
858+</D:response>
859+<D:href>http://localhost/toto</D:href>
860+</D:multistatus>"""
861+ self.assertRaises(errors.NotADirectory,
862+ self._extract_dir_content_from_str, example)
863+
864+ def test_list_dir_apache2_example(self):
865+ example = _get_list_dir_apache2_depth_1_prop()
866+ self.assertRaises(errors.NotADirectory,
867+ self._extract_dir_content_from_str, example)
868+
869+ def test_list_dir_lighttpd_example(self):
870+ example = """<?xml version="1.0" encoding="utf-8"?>
871+<D:multistatus xmlns:D="DAV:" xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
872+<D:response>
873+<D:href>http://localhost/</D:href>
874+</D:response>
875+<D:response>
876+<D:href>http://localhost/titi</D:href>
877+</D:response>
878+<D:response>
879+<D:href>http://localhost/toto</D:href>
880+</D:response>
881+</D:multistatus>"""
882+ self.assertRaises(errors.NotADirectory,
883+ self._extract_dir_content_from_str, example)
884+
885+ def test_list_dir_apache2_dir_depth_1_example(self):
886+ example = _get_list_dir_apache2_depth_1_allprop()
887+ self.assertEquals([('executable', False, 14, True),
888+ ('read-only', False, 42, False),
889+ ('titi', False, 6, False),
890+ ('toto', True, -1, False)],
891+ self._extract_dir_content_from_str(example))
892+
893+ def test_stat_malformed_response(self):
894+ # Invalid xml, neither multistatus nor response are properly closed
895+ example = """<?xml version="1.0" encoding="utf-8"?>
896+<D:multistatus xmlns:D="DAV:" xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
897+<D:response>
898+<D:href>http://localhost/</D:href>"""
899+ self.assertRaises(errors.InvalidHttpResponse,
900+ self._extract_stat_from_str, example)
901+
902+ def test_stat_incomplete_format_response(self):
903+ # The minimal information is present but doesn't conform to RFC 2518
904+ # (well, as I understand it since the reference servers disagree on
905+ # more than details).
906+
907+ # The href below is not enclosed in a response element and is
908+ # therefore ignored.
909+ example = """<?xml version="1.0" encoding="utf-8"?>
910+<D:multistatus xmlns:D="DAV:" xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
911+<D:href>http://localhost/toto</D:href>
912+</D:multistatus>"""
913+ self.assertRaises(errors.InvalidHttpResponse,
914+ self._extract_stat_from_str, example)
915+
916+ def test_stat_apache2_file_example(self):
917+ example = """<?xml version="1.0" encoding="utf-8"?>
918+<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">
919+<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
920+<D:href>/executable</D:href>
921+<D:propstat>
922+<D:prop>
923+<lp1:resourcetype/>
924+<lp1:creationdate>2008-06-08T09:50:15Z</lp1:creationdate>
925+<lp1:getcontentlength>12</lp1:getcontentlength>
926+<lp1:getlastmodified>Sun, 08 Jun 2008 09:50:11 GMT</lp1:getlastmodified>
927+<lp1:getetag>"da9f81-0-9ef33ac0"</lp1:getetag>
928+<lp2:executable>T</lp2:executable>
929+<D:supportedlock>
930+<D:lockentry>
931+<D:lockscope><D:exclusive/></D:lockscope>
932+<D:locktype><D:write/></D:locktype>
933+</D:lockentry>
934+<D:lockentry>
935+<D:lockscope><D:shared/></D:lockscope>
936+<D:locktype><D:write/></D:locktype>
937+</D:lockentry>
938+</D:supportedlock>
939+<D:lockdiscovery/>
940+</D:prop>
941+<D:status>HTTP/1.1 200 OK</D:status>
942+</D:propstat>
943+</D:response>
944+</D:multistatus>"""
945+ st = self._extract_stat_from_str(example)
946+ self.assertEquals(12, st.st_size)
947+ self.assertFalse(stat.S_ISDIR(st.st_mode))
948+ self.assertTrue(stat.S_ISREG(st.st_mode))
949+ self.assertTrue(st.st_mode & stat.S_IXUSR)
950+
951+ def test_stat_apache2_dir_depth_1_example(self):
952+ example = _get_list_dir_apache2_depth_1_allprop()
953+ self.assertRaises(errors.InvalidHttpResponse,
954+ self._extract_stat_from_str, example)
955+
956+ def test_stat_apache2_dir_depth_0_example(self):
957+ example = """<?xml version="1.0" encoding="utf-8"?>
958+<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">
959+<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
960+<D:href>/</D:href>
961+<D:propstat>
962+<D:prop>
963+<lp1:resourcetype><D:collection/></lp1:resourcetype>
964+<lp1:creationdate>2008-06-08T10:50:38Z</lp1:creationdate>
965+<lp1:getlastmodified>Sun, 08 Jun 2008 10:50:38 GMT</lp1:getlastmodified>
966+<lp1:getetag>"da7f5a-cc-7722db80"</lp1:getetag>
967+<D:supportedlock>
968+<D:lockentry>
969+<D:lockscope><D:exclusive/></D:lockscope>
970+<D:locktype><D:write/></D:locktype>
971+</D:lockentry>
972+<D:lockentry>
973+<D:lockscope><D:shared/></D:lockscope>
974+<D:locktype><D:write/></D:locktype>
975+</D:lockentry>
976+</D:supportedlock>
977+<D:lockdiscovery/>
978+</D:prop>
979+<D:status>HTTP/1.1 200 OK</D:status>
980+</D:propstat>
981+</D:response>
982+</D:multistatus>
983+"""
984+ st = self._extract_stat_from_str(example)
985+ self.assertEquals(-1, st.st_size)
986+ self.assertTrue(stat.S_ISDIR(st.st_mode))
987+ self.assertTrue(st.st_mode & stat.S_IXUSR)
988
989=== added file 'bzrlib/plugins/webdav/webdav.py'
990--- bzrlib/plugins/webdav/webdav.py 1970-01-01 00:00:00 +0000
991+++ bzrlib/plugins/webdav/webdav.py 2012-03-26 16:57:22 +0000
992@@ -0,0 +1,873 @@
993+# Copyright (C) 2006-2009, 2011, 2012 Canonical Ltd
994+#
995+# This program is free software; you can redistribute it and/or modify
996+# it under the terms of the GNU General Public License as published by
997+# the Free Software Foundation; either version 2 of the License, or
998+# (at your option) any later version.
999+#
1000+# This program is distributed in the hope that it will be useful,
1001+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1002+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1003+# GNU General Public License for more details.
1004+#
1005+# You should have received a copy of the GNU General Public License
1006+# along with this program; if not, write to the Free Software
1007+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
1008+
1009+"""Implementation of WebDAV for http transports.
1010+
1011+A Transport which complement http transport by implementing
1012+partially the WebDAV protocol to push files.
1013+This should enable remote push operations.
1014+"""
1015+
1016+from __future__ import absolute_import
1017+from cStringIO import StringIO
1018+import os
1019+import random
1020+import re
1021+import sys
1022+import time
1023+import urllib2
1024+import xml.sax
1025+import xml.sax.handler
1026+
1027+
1028+from bzrlib import (
1029+ errors,
1030+ osutils,
1031+ trace,
1032+ transport,
1033+ urlutils,
1034+ )
1035+from bzrlib.transport.http import (
1036+ _urllib,
1037+ _urllib2_wrappers,
1038+ )
1039+
1040+
1041+class DavResponseHandler(xml.sax.handler.ContentHandler):
1042+ """Handle a multi-status DAV response."""
1043+
1044+ def __init__(self):
1045+ self.url = None
1046+ self.elt_stack = None
1047+ self.chars = None
1048+ self.chars_wanted = False
1049+ self.expected_content_handled = False
1050+
1051+ def set_url(self, url):
1052+ """Set the url used for error reporting when handling a response."""
1053+ self.url = url
1054+
1055+ def startDocument(self):
1056+ self.elt_stack = []
1057+ self.chars = None
1058+ self.expected_content_handled = False
1059+
1060+ def endDocument(self):
1061+ self._validate_handling()
1062+ if not self.expected_content_handled:
1063+ raise errors.InvalidHttpResponse(self.url,
1064+ msg='Unknown xml response')
1065+
1066+ def startElement(self, name, attrs):
1067+ self.elt_stack.append(self._strip_ns(name))
1068+ # The following is incorrect in the general case where elements are
1069+ # intermixed with chars in a higher level element. That's not the case
1070+ # here (otherwise the chars_wanted will have to be stacked too).
1071+ if self.chars_wanted:
1072+ self.chars = ''
1073+ else:
1074+ self.chars = None
1075+
1076+ def endElement(self, name):
1077+ self.chars = None
1078+ self.chars_wanted = False
1079+ self.elt_stack.pop()
1080+
1081+ def characters(self, chrs):
1082+ if self.chars_wanted:
1083+ self.chars += chrs
1084+
1085+ def _current_element(self):
1086+ return self.elt_stack[-1]
1087+
1088+ def _strip_ns(self, name):
1089+ """Strip the leading namespace from name.
1090+
1091+ We don't have namespaces clashes in our context, stripping it makes the
1092+ code simpler.
1093+ """
1094+ where = name.find(':')
1095+ if where == -1:
1096+ return name
1097+ else:
1098+ return name[where +1:]
1099+
1100+
1101+class DavStatHandler(DavResponseHandler):
1102+ """Handle a PROPPFIND DAV response for a file or directory.
1103+
1104+ The expected content is:
1105+ - a multi-status element containing
1106+ - a single response element containing
1107+ - a href element
1108+ - a propstat element containing
1109+ - a status element (ignored)
1110+ - a prop element containing at least (other are ignored)
1111+ - a getcontentlength element (for files only)
1112+ - an executable element (for files only)
1113+ - a resourcetype element containing
1114+ - a collection element (for directories only)
1115+ """
1116+
1117+ def __init__(self):
1118+ DavResponseHandler.__init__(self)
1119+ # Flags defining the context for the actions
1120+ self._response_seen = False
1121+ self._init_response_attrs()
1122+
1123+ def _init_response_attrs(self):
1124+ self.href = None
1125+ self.length = -1
1126+ self.executable = None
1127+ self.is_dir = False
1128+
1129+ def _validate_handling(self):
1130+ if self.href is not None:
1131+ self.expected_content_handled = True
1132+
1133+ def startElement(self, name, attrs):
1134+ sname = self._strip_ns(name)
1135+ self.chars_wanted = sname in ('href', 'getcontentlength', 'executable')
1136+ DavResponseHandler.startElement(self, name, attrs)
1137+
1138+ def endElement(self, name):
1139+ if self._response_seen:
1140+ self._additional_response_starting(name)
1141+
1142+ if self._href_end():
1143+ self.href = self.chars
1144+ elif self._getcontentlength_end():
1145+ self.length = int(self.chars)
1146+ elif self._executable_end():
1147+ self.executable = self.chars
1148+ elif self._collection_end():
1149+ self.is_dir = True
1150+
1151+ if self._strip_ns(name) == 'response':
1152+ self._response_seen = True
1153+ self._response_handled()
1154+ DavResponseHandler.endElement(self, name)
1155+
1156+
1157+ def _response_handled(self):
1158+ """A response element inside a multistatus have been parsed."""
1159+ pass
1160+
1161+ def _additional_response_starting(self, name):
1162+ """A additional response element inside a multistatus begins."""
1163+ sname = self._strip_ns(name)
1164+ if sname != 'multistatus':
1165+ raise errors.InvalidHttpResponse(
1166+ self.url, msg='Unexpected %s element' % name)
1167+
1168+ def _href_end(self):
1169+ stack = self.elt_stack
1170+ return (len(stack) == 3
1171+ and stack[0] == 'multistatus'
1172+ and stack[1] == 'response'
1173+ and stack[2] == 'href')
1174+
1175+ def _getcontentlength_end(self):
1176+ stack = self.elt_stack
1177+ return (len(stack) == 5
1178+ and stack[0] == 'multistatus'
1179+ and stack[1] == 'response'
1180+ and stack[2] == 'propstat'
1181+ and stack[3] == 'prop'
1182+ and stack[4] == 'getcontentlength')
1183+
1184+ def _executable_end(self):
1185+ stack = self.elt_stack
1186+ return (len(stack) == 5
1187+ and stack[0] == 'multistatus'
1188+ and stack[1] == 'response'
1189+ and stack[2] == 'propstat'
1190+ and stack[3] == 'prop'
1191+ and stack[4] == 'executable')
1192+
1193+ def _collection_end(self):
1194+ stack = self.elt_stack
1195+ return (len(stack) == 6
1196+ and stack[0] == 'multistatus'
1197+ and stack[1] == 'response'
1198+ and stack[2] == 'propstat'
1199+ and stack[3] == 'prop'
1200+ and stack[4] == 'resourcetype'
1201+ and stack[5] == 'collection')
1202+
1203+
1204+class _DAVStat(object):
1205+ """The stat info as it can be acquired with DAV."""
1206+
1207+ def __init__(self, size, is_dir, is_exec):
1208+ self.st_size = size
1209+ # We build a mode considering that:
1210+
1211+ # - we have no idea about group or other chmod bits so we use a sane
1212+ # default (bzr should not care anyway)
1213+
1214+ # - we suppose that the user can write
1215+ if is_dir:
1216+ self.st_mode = 0040644
1217+ else:
1218+ self.st_mode = 0100644
1219+ if is_exec:
1220+ self.st_mode = self.st_mode | 0755
1221+
1222+
1223+def _extract_stat_info(url, infile):
1224+ """Extract the stat-like information from a DAV PROPFIND response.
1225+
1226+ :param url: The url used for the PROPFIND request.
1227+ :param infile: A file-like object pointing at the start of the response.
1228+ """
1229+ parser = xml.sax.make_parser()
1230+
1231+ handler = DavStatHandler()
1232+ handler.set_url(url)
1233+ parser.setContentHandler(handler)
1234+ try:
1235+ parser.parse(infile)
1236+ except xml.sax.SAXParseException, e:
1237+ raise errors.InvalidHttpResponse(
1238+ url, msg='Malformed xml response: %s' % e)
1239+ if handler.is_dir:
1240+ size = -1 # directory sizes are meaningless for bzr
1241+ is_exec = True
1242+ else:
1243+ size = handler.length
1244+ is_exec = (handler.executable == 'T')
1245+ return _DAVStat(size, handler.is_dir, is_exec)
1246+
1247+
1248+class DavListDirHandler(DavStatHandler):
1249+ """Handle a PROPPFIND depth 1 DAV response for a directory."""
1250+ def __init__(self):
1251+ DavStatHandler.__init__(self)
1252+ self.dir_content = None
1253+
1254+ def _validate_handling(self):
1255+ if self.dir_content is not None:
1256+ self.expected_content_handled = True
1257+
1258+ def _make_response_tuple(self):
1259+ if self.executable == 'T':
1260+ is_exec = True
1261+ else:
1262+ is_exec = False
1263+ return (self.href, self.is_dir, self.length, is_exec)
1264+
1265+ def _response_handled(self):
1266+ """A response element inside a multistatus have been parsed."""
1267+ if self.dir_content is None:
1268+ self.dir_content = []
1269+ self.dir_content.append(self._make_response_tuple())
1270+ # Resest the attributes for the next response if any
1271+ self._init_response_attrs()
1272+
1273+ def _additional_response_starting(self, name):
1274+ """A additional response element inside a multistatus begins."""
1275+ pass
1276+
1277+
1278+def _extract_dir_content(url, infile):
1279+ """Extract the directory content from a DAV PROPFIND response.
1280+
1281+ :param url: The url used for the PROPFIND request.
1282+ :param infile: A file-like object pointing at the start of the response.
1283+ """
1284+ parser = xml.sax.make_parser()
1285+
1286+ handler = DavListDirHandler()
1287+ handler.set_url(url)
1288+ parser.setContentHandler(handler)
1289+ try:
1290+ parser.parse(infile)
1291+ except xml.sax.SAXParseException, e:
1292+ raise errors.InvalidHttpResponse(
1293+ url, msg='Malformed xml response: %s' % e)
1294+ # Reformat for bzr needs
1295+ dir_content = handler.dir_content
1296+ (dir_name, is_dir) = dir_content[0][:2]
1297+ if not is_dir:
1298+ raise errors.NotADirectory(url)
1299+ dir_len = len(dir_name)
1300+ elements = []
1301+ for (href, is_dir, size, is_exec) in dir_content[1:]: # Ignore first element
1302+ if href.startswith(dir_name):
1303+ name = href[dir_len:]
1304+ if name.endswith('/'):
1305+ # Get rid of final '/'
1306+ name = name[0:-1]
1307+ # We receive already url-encoded strings so down-casting is
1308+ # safe. And bzr insists on getting strings not unicode strings.
1309+ elements.append((str(name), is_dir, size, is_exec))
1310+ return elements
1311+
1312+
1313+class PUTRequest(_urllib2_wrappers.Request):
1314+
1315+ def __init__(self, url, data, more_headers={}, accepted_errors=None):
1316+ # FIXME: Accept */* ? Why ? *we* send, we do not receive :-/
1317+ headers = {'Accept': '*/*',
1318+ 'Content-type': 'application/octet-stream',
1319+ # FIXME: We should complete the
1320+ # implementation of
1321+ # htmllib.HTTPConnection, it's just a
1322+ # shame (at least a waste) that we
1323+ # can't use the following.
1324+
1325+ # 'Expect': '100-continue',
1326+ # 'Transfer-Encoding': 'chunked',
1327+ }
1328+ headers.update(more_headers)
1329+ _urllib2_wrappers.Request.__init__(self, 'PUT', url, data, headers,
1330+ accepted_errors=accepted_errors)
1331+
1332+
1333+class DavResponse(_urllib2_wrappers.Response):
1334+ """Custom HTTPResponse.
1335+
1336+ DAV have some reponses for which the body is of no interest.
1337+ """
1338+ _body_ignored_responses = (
1339+ _urllib2_wrappers.Response._body_ignored_responses
1340+ + [201, 405, 409, 412,]
1341+ )
1342+
1343+ def begin(self):
1344+ """Begin to read the response from the server.
1345+
1346+ httplib incorrectly close the connection far too easily. Let's try to
1347+ workaround that (as _urllib2 does, but for more cases...).
1348+ """
1349+ _urllib2_wrappers.Response.begin(self)
1350+ if self.status in (201, 204):
1351+ self.will_close = False
1352+
1353+
1354+# Takes DavResponse into account:
1355+class DavHTTPConnection(_urllib2_wrappers.HTTPConnection):
1356+
1357+ response_class = DavResponse
1358+
1359+
1360+class DavHTTPSConnection(_urllib2_wrappers.HTTPSConnection):
1361+
1362+ response_class = DavResponse
1363+
1364+
1365+class DavConnectionHandler(_urllib2_wrappers.ConnectionHandler):
1366+ """Custom connection handler.
1367+
1368+ We need to use the DavConnectionHTTPxConnection class to take
1369+ into account our own DavResponse objects, to be able to
1370+ declare our own body ignored responses, sigh.
1371+ """
1372+
1373+ def http_request(self, request):
1374+ return self.capture_connection(request, DavHTTPConnection)
1375+
1376+ def https_request(self, request):
1377+ return self.capture_connection(request, DavHTTPSConnection)
1378+
1379+
1380+class DavOpener(_urllib2_wrappers.Opener):
1381+ """Dav specific needs regarding HTTP(S)"""
1382+
1383+ def __init__(self, report_activity=None, ca_certs=None):
1384+ super(DavOpener, self).__init__(connection=DavConnectionHandler,
1385+ report_activity=report_activity,
1386+ ca_certs=ca_certs)
1387+
1388+
1389+class HttpDavTransport(_urllib.HttpTransport_urllib):
1390+ """An transport able to put files using http[s] on a DAV server.
1391+
1392+ We don't try to implement the whole WebDAV protocol. Just the minimum
1393+ needed for bzr.
1394+ """
1395+
1396+ _debuglevel = 0
1397+ _opener_class = DavOpener
1398+
1399+ def is_readonly(self):
1400+ """See Transport.is_readonly."""
1401+ return False
1402+
1403+ def _raise_http_error(self, url, response, info=None):
1404+ if info is None:
1405+ msg = ''
1406+ else:
1407+ msg = ': ' + info
1408+ raise errors.InvalidHttpResponse(url, 'Unable to handle http code %d%s'
1409+ % (response.code, msg))
1410+
1411+ def _handle_common_errors(self, code, abspath):
1412+ if code == 404:
1413+ raise errors.NoSuchFile(abspath)
1414+
1415+ def open_write_stream(self, relpath, mode=None):
1416+ """See Transport.open_write_stream."""
1417+ # FIXME: this implementation sucks, we should really use chunk encoding
1418+ # and buffers.
1419+ self.put_bytes(relpath, "", mode)
1420+ result = transport.AppendBasedFileStream(self, relpath)
1421+ transport._file_streams[self.abspath(relpath)] = result
1422+ return result
1423+
1424+ def put_file(self, relpath, f, mode=None):
1425+ """See Transport.put_file"""
1426+ # FIXME: We read the whole file in memory, using chunked encoding and
1427+ # counting bytes while sending them will be far better. Look at reusing
1428+ # osutils.pumpfile ?
1429+ #
1430+ bytes = f.read()
1431+ self.put_bytes(relpath, bytes, mode=None)
1432+ return len(bytes)
1433+
1434+ def put_bytes(self, relpath, bytes, mode=None):
1435+ """Copy the bytes object into the location.
1436+
1437+ Tests revealed that contrary to what is said in
1438+ http://www.rfc.net/rfc2068.html, the put is not
1439+ atomic. When putting a file, if the client died, a
1440+ partial file may still exists on the server.
1441+
1442+ So we first put a temp file and then move it.
1443+
1444+ :param relpath: Location to put the contents, relative to base.
1445+ :param f: File-like object.
1446+ :param mode: Not supported by DAV.
1447+ """
1448+ abspath = self._remote_path(relpath)
1449+
1450+ # We generate a sufficiently random name to *assume* that
1451+ # no collisions will occur and don't worry about it (nor
1452+ # handle it).
1453+ stamp = '.tmp.%.9f.%d.%d' % (time.time(),
1454+ os.getpid(),
1455+ random.randint(0,0x7FFFFFFF))
1456+ # A temporary file to hold all the data to guard against
1457+ # client death
1458+ tmp_relpath = relpath + stamp
1459+
1460+ # Will raise if something gets wrong
1461+ self.put_bytes_non_atomic(tmp_relpath, bytes)
1462+
1463+ # Now move the temp file
1464+ try:
1465+ self.move(tmp_relpath, relpath)
1466+ except Exception, e:
1467+ # If we fail, try to clean up the temporary file
1468+ # before we throw the exception but don't let another
1469+ # exception mess things up.
1470+ exc_type, exc_val, exc_tb = sys.exc_info()
1471+ try:
1472+ self.delete(tmp_relpath)
1473+ except:
1474+ raise exc_type, exc_val, exc_tb
1475+ raise # raise the original with its traceback if we can.
1476+
1477+ def put_file_non_atomic(self, relpath, f,
1478+ mode=None,
1479+ create_parent_dir=False,
1480+ dir_mode=False):
1481+ # Implementing put_bytes_non_atomic rather than put_file_non_atomic
1482+ # because to do a put request, we must read all of the file into
1483+ # RAM anyway. Better to do that than to have the contents, put
1484+ # into a StringIO() and then read them all out again later.
1485+ self.put_bytes_non_atomic(relpath, f.read(), mode=mode,
1486+ create_parent_dir=create_parent_dir,
1487+ dir_mode=dir_mode)
1488+
1489+ def put_bytes_non_atomic(self, relpath, bytes,
1490+ mode=None,
1491+ create_parent_dir=False,
1492+ dir_mode=False):
1493+ """See Transport.put_file_non_atomic"""
1494+
1495+ abspath = self._remote_path(relpath)
1496+ request = PUTRequest(abspath, bytes,
1497+ accepted_errors=[200, 201, 204, 403, 404, 409])
1498+
1499+ def bare_put_file_non_atomic():
1500+
1501+ response = self._perform(request)
1502+ code = response.code
1503+
1504+ if code in (403, 404, 409):
1505+ # Intermediate directories missing
1506+ raise errors.NoSuchFile(abspath)
1507+ if code not in (200, 201, 204):
1508+ self._raise_curl_http_error(abspath, response,
1509+ 'expected 200, 201 or 204.')
1510+
1511+ try:
1512+ bare_put_file_non_atomic()
1513+ except errors.NoSuchFile:
1514+ if not create_parent_dir:
1515+ raise
1516+ parent_dir = osutils.dirname(relpath)
1517+ if parent_dir:
1518+ self.mkdir(parent_dir, mode=dir_mode)
1519+ return bare_put_file_non_atomic()
1520+ else:
1521+ # Don't forget to re-raise if the parent dir doesn't exist
1522+ raise
1523+
1524+ def _put_bytes_ranged(self, relpath, bytes, at):
1525+ """Append the file-like object part to the end of the location.
1526+
1527+ :param relpath: Location to put the contents, relative to base.
1528+ :param bytes: A string of bytes to upload
1529+ :param at: The position in the file to add the bytes
1530+ """
1531+ # Acquire just the needed data
1532+ # TODO: jam 20060908 Why are we creating a StringIO to hold the
1533+ # data, and then using data.read() to send the data
1534+ # in the PUTRequest. Rather than just reading in and
1535+ # uploading the data.
1536+ # Also, if we have to read the whole file into memory anyway
1537+ # it would be better to implement put_bytes(), and redefine
1538+ # put_file as self.put_bytes(relpath, f.read())
1539+
1540+ # Once we teach httplib to do that, we will use file-like
1541+ # objects (see handling chunked data and 100-continue).
1542+ abspath = self._remote_path(relpath)
1543+
1544+ # Content-Range is start-end/size. 'size' is the file size, not the
1545+ # chunk size. We can't be sure about the size of the file so put '*' at
1546+ # the end of the range instead.
1547+ request = PUTRequest(abspath, bytes,
1548+ {'Content-Range':
1549+ 'bytes %d-%d/*' % (at, at+len(bytes)-1),},
1550+ accepted_errors=[200, 201, 204, 403, 404, 409])
1551+ response = self._perform(request)
1552+ code = response.code
1553+
1554+ if code in (403, 404, 409):
1555+ raise errors.NoSuchFile(abspath) # Intermediate directories missing
1556+ if code not in (200, 201, 204):
1557+ self._raise_http_error(abspath, response,
1558+ 'expected 200, 201 or 204.')
1559+
1560+ def mkdir(self, relpath, mode=None):
1561+ """See Transport.mkdir"""
1562+ abspath = self._remote_path(relpath)
1563+
1564+ request = _urllib2_wrappers.Request('MKCOL', abspath,
1565+ accepted_errors=[201, 403, 405,
1566+ 404, 409])
1567+ response = self._perform(request)
1568+
1569+ code = response.code
1570+ # jam 20060908: The error handling seems to be repeated for
1571+ # each function. Is it possible to factor it out into
1572+ # a helper rather than repeat it for each one?
1573+ # (I realize there is some custom behavior)
1574+ # Yes it is and will be done.
1575+ if code == 403:
1576+ # Forbidden (generally server misconfigured or not
1577+ # configured for DAV)
1578+ raise self._raise_http_error(abspath, response, 'mkdir failed')
1579+ elif code == 405:
1580+ # Not allowed (generally already exists)
1581+ raise errors.FileExists(abspath)
1582+ elif code in (404, 409):
1583+ # Conflict (intermediate directories do not exist)
1584+ raise errors.NoSuchFile(abspath)
1585+ elif code != 201: # Created
1586+ raise self._raise_http_error(abspath, response, 'mkdir failed')
1587+
1588+ def rename(self, rel_from, rel_to):
1589+ """Rename without special overwriting"""
1590+ abs_from = self._remote_path(rel_from)
1591+ abs_to = self._remote_path(rel_to)
1592+
1593+ request = _urllib2_wrappers.Request('MOVE', abs_from, None,
1594+ {'Destination': abs_to,
1595+ 'Overwrite': 'F'},
1596+ accepted_errors=[201, 404, 409,
1597+ 412])
1598+ response = self._perform(request)
1599+
1600+ code = response.code
1601+ if code == 404:
1602+ raise errors.NoSuchFile(abs_from)
1603+ if code == 412:
1604+ raise errors.FileExists(abs_to)
1605+ if code == 409:
1606+ # More precisely some intermediate directories are missing
1607+ raise errors.NoSuchFile(abs_to)
1608+ if code != 201:
1609+ # As we don't want to accept overwriting abs_to, 204
1610+ # (meaning abs_to was existing (but empty, the
1611+ # non-empty case is 412)) will be an error, a server
1612+ # bug even, since we require explicitely to not
1613+ # overwrite.
1614+ self._raise_http_error(abs_from, response,
1615+ 'unable to rename to %r' % (abs_to))
1616+ def move(self, rel_from, rel_to):
1617+ """See Transport.move"""
1618+
1619+ abs_from = self._remote_path(rel_from)
1620+ abs_to = self._remote_path(rel_to)
1621+
1622+ request = _urllib2_wrappers.Request('MOVE', abs_from, None,
1623+ {'Destination': abs_to},
1624+ accepted_errors=[201, 204,
1625+ 404, 409])
1626+ response = self._perform(request)
1627+
1628+ code = response.code
1629+ if code == 404:
1630+ raise errors.NoSuchFile(abs_from)
1631+ if code == 409:
1632+ raise errors.DirectoryNotEmpty(abs_to)
1633+ # Overwriting allowed, 201 means abs_to did not exist,
1634+ # 204 means it did exist.
1635+ if code not in (201, 204):
1636+ self._raise_http_error(abs_from, response,
1637+ 'unable to move to %r' % (abs_to))
1638+
1639+ def delete(self, rel_path):
1640+ """
1641+ Delete the item at relpath.
1642+
1643+ Note that when a non-empty dir required to be deleted, a conforming DAV
1644+ server will delete the dir and all its content. That does not normally
1645+ happen in bzr.
1646+ """
1647+ abs_path = self._remote_path(rel_path)
1648+
1649+ request = _urllib2_wrappers.Request('DELETE', abs_path,
1650+ accepted_errors=[200, 204,
1651+ 404, 999])
1652+ response = self._perform(request)
1653+
1654+ code = response.code
1655+ if code == 404:
1656+ raise errors.NoSuchFile(abs_path)
1657+ if code != 204:
1658+ self._raise_curl_http_error(curl, 'unable to delete')
1659+
1660+ def copy(self, rel_from, rel_to):
1661+ """See Transport.copy"""
1662+ abs_from = self._remote_path(rel_from)
1663+ abs_to = self._remote_path(rel_to)
1664+
1665+ request = _urllib2_wrappers.Request(
1666+ 'COPY', abs_from, None,
1667+ {'Destination': abs_to},
1668+ accepted_errors=[201, 204, 404, 409])
1669+ response = self._perform(request)
1670+
1671+ code = response.code
1672+ if code in (404, 409):
1673+ raise errors.NoSuchFile(abs_from)
1674+ # XXX: our test server returns 201 but apache2 returns 204, need
1675+ # investivation.
1676+ if code not in(201, 204):
1677+ self._raise_http_error(abs_from, response,
1678+ 'unable to copy from %r to %r'
1679+ % (abs_from,abs_to))
1680+
1681+ def copy_to(self, relpaths, other, mode=None, pb=None):
1682+ """Copy a set of entries from self into another Transport.
1683+
1684+ :param relpaths: A list/generator of entries to be copied.
1685+ """
1686+ # DavTransport can be a target. So our simple implementation
1687+ # just returns the Transport implementation. (Which just does
1688+ # a put(get())
1689+ # We only override, because the default HttpTransportBase, explicitly
1690+ # disabled it for HTTP
1691+ return transport.Transport.copy_to(self, relpaths, other,
1692+ mode=mode, pb=pb)
1693+
1694+ def listable(self):
1695+ """See Transport.listable."""
1696+ return True
1697+
1698+ def list_dir(self, relpath):
1699+ """
1700+ Return a list of all files at the given location.
1701+ """
1702+ return [elt[0] for elt in self._list_tree(relpath, 1)]
1703+
1704+ def _list_tree(self, relpath, depth):
1705+ abspath = self._remote_path(relpath)
1706+ propfind = """<?xml version="1.0" encoding="utf-8" ?>
1707+ <D:propfind xmlns:D="DAV:">
1708+ <D:allprop/>
1709+ </D:propfind>
1710+"""
1711+ request = _urllib2_wrappers.Request(
1712+ 'PROPFIND', abspath, propfind,
1713+ {'Depth': '%s' % (depth,),
1714+ 'Content-Type': 'application/xml; charset="utf-8"'},
1715+ accepted_errors=[207, 404, 409,])
1716+ response = self._perform(request)
1717+
1718+ code = response.code
1719+ if code == 404:
1720+ raise errors.NoSuchFile(abspath)
1721+ if code == 409:
1722+ # More precisely some intermediate directories are missing
1723+ raise errors.NoSuchFile(abspath)
1724+ if code != 207:
1725+ self._raise_http_error(abspath, response,
1726+ 'unable to list %r directory' % (abspath))
1727+ return _extract_dir_content(abspath, response)
1728+
1729+ def lock_write(self, relpath):
1730+ """Lock the given file for exclusive access.
1731+ :return: A lock object, which should be passed to Transport.unlock()
1732+ """
1733+ # We follow the same path as FTP, which just returns a BogusLock
1734+ # object. We don't explicitly support locking a specific file.
1735+ # TODO: jam 2006-09-08 SFTP implements this by opening exclusive
1736+ # "relpath + '.lock_write'". Does DAV implement anything like
1737+ # O_EXCL?
1738+ # Alternatively, LocalTransport uses an OS lock to lock the file
1739+ # and WebDAV supports some sort of locking.
1740+ return self.lock_read(relpath)
1741+
1742+ def rmdir(self, relpath):
1743+ """See Transport.rmdir."""
1744+ content = self.list_dir(relpath)
1745+ if len(content) > 0:
1746+ raise errors.DirectoryNotEmpty(self._remote_path(relpath))
1747+ self.delete(relpath)
1748+
1749+ def stat(self, relpath):
1750+ """See Transport.stat.
1751+
1752+ We provide a limited implementation for bzr needs.
1753+ """
1754+ abspath = self._remote_path(relpath)
1755+ propfind = """<?xml version="1.0" encoding="utf-8" ?>
1756+ <D:propfind xmlns:D="DAV:">
1757+ <D:allprop/>
1758+ </D:propfind>
1759+"""
1760+ request = _urllib2_wrappers.Request(
1761+ 'PROPFIND', abspath, propfind,
1762+ {'Depth': '0', 'Content-Type': 'application/xml; charset="utf-8"'},
1763+ accepted_errors=[207, 404, 409,])
1764+
1765+ response = self._perform(request)
1766+
1767+ code = response.code
1768+ if code == 404:
1769+ raise errors.NoSuchFile(abspath)
1770+ if code == 409:
1771+ # FIXME: Could this really occur ?
1772+ # More precisely some intermediate directories are missing
1773+ raise errors.NoSuchFile(abspath)
1774+ if code != 207:
1775+ self._raise_http_error(abspath, response,
1776+ 'unable to list %r directory' % (abspath))
1777+ return _extract_stat_info(abspath, response)
1778+
1779+ def iter_files_recursive(self):
1780+ """Walk the relative paths of all files in this transport."""
1781+ # We get the whole tree with a single request
1782+ tree = self._list_tree('.', 'Infinity')
1783+ # Now filter out the directories
1784+ for (name, is_dir, size, is_exex) in tree:
1785+ if not is_dir:
1786+ yield name
1787+
1788+ def append_file(self, relpath, f, mode=None):
1789+ """See Transport.append_file"""
1790+ return self.append_bytes(relpath, f.read(), mode=mode)
1791+
1792+ def append_bytes(self, relpath, bytes, mode=None):
1793+ """See Transport.append_bytes"""
1794+ if self._range_hint is not None:
1795+ # TODO: We reuse the _range_hint handled by bzr core,
1796+ # unless someone can show me a server implementing
1797+ # range for write but not for read. But we may, on
1798+ # our own, try to handle a similar flag for write
1799+ # ranges supported by a given server. Or at least,
1800+ # detect that ranges are not correctly handled and
1801+ # fallback to no ranges.
1802+ before = self._append_by_head_put(relpath, bytes)
1803+ else:
1804+ before = self._append_by_get_put(relpath, bytes)
1805+ return before
1806+
1807+ def _append_by_head_put(self, relpath, bytes):
1808+ """Append without getting the whole file.
1809+
1810+ When the server allows it, a 'Content-Range' header can be specified.
1811+ """
1812+ response = self._head(relpath)
1813+ code = response.code
1814+ if code == 404:
1815+ relpath_size = 0
1816+ else:
1817+ # Consider the absence of Content-Length header as
1818+ # indicating an existing but empty file (Apache 2.0
1819+ # does this, and there is even a comment in
1820+ # modules/http/http_protocol.c calling that a *hack*,
1821+ # I agree, it's a hack. On the other hand if the file
1822+ # do not exist we get a 404, if the file does exist,
1823+ # is not empty and we get no Content-Length header,
1824+ # then the server is buggy :-/ )
1825+ relpath_size = int(response.headers.get('Content-Length', 0))
1826+ if relpath_size == 0:
1827+ trace.mutter('if %s is not empty, the server is buggy'
1828+ % relpath)
1829+ if relpath_size:
1830+ self._put_bytes_ranged(relpath, bytes, relpath_size)
1831+ else:
1832+ self.put_bytes(relpath, bytes)
1833+
1834+ return relpath_size
1835+
1836+ def _append_by_get_put(self, relpath, bytes):
1837+ # So we need to GET the file first, append to it and
1838+ # finally PUT back the result.
1839+ full_data = StringIO()
1840+ try:
1841+ data = self.get(relpath)
1842+ full_data.write(data.read())
1843+ except errors.NoSuchFile:
1844+ # Good, just do the put then
1845+ pass
1846+
1847+ # Append the f content
1848+ before = full_data.tell()
1849+ full_data.write(bytes)
1850+ full_data.seek(0)
1851+
1852+ self.put_file(relpath, full_data)
1853+
1854+ return before
1855+
1856+ def get_smart_medium(self):
1857+ # smart server and webdav are exclusive. There is really no point to
1858+ # use webdav if a smart server is available
1859+ raise errors.NoSmartMedium(self)
1860+
1861+
1862+def get_test_permutations():
1863+ """Return the permutations to be used in testing."""
1864+ from bzrlib.plugins.webdav.tests import dav_server
1865+ return [(HttpDavTransport, dav_server.DAVServer),]
1866
1867=== modified file 'doc/en/release-notes/bzr-2.6.txt'
1868--- doc/en/release-notes/bzr-2.6.txt 2012-03-16 15:05:05 +0000
1869+++ doc/en/release-notes/bzr-2.6.txt 2012-03-26 16:57:22 +0000
1870@@ -20,6 +20,8 @@
1871
1872 .. New commands, options, etc that users may wish to try out.
1873
1874+* The webdav plugin is now a core plugin. (Vincent Ladeuil)
1875+
1876 Improvements
1877 ************
1878
1879
1880=== modified file 'doc/en/whats-new/whats-new-in-2.6.txt'
1881--- doc/en/whats-new/whats-new-in-2.6.txt 2012-01-16 13:49:34 +0000
1882+++ doc/en/whats-new/whats-new-in-2.6.txt 2012-03-26 16:57:22 +0000
1883@@ -16,7 +16,12 @@
1884 2.1, 2.2, 2.3, 2.4 and 2.5, and can read and write repositories generated by
1885 all previous versions.
1886
1887-<topics of interest here>
1888+webdav support
1889+**************
1890+
1891+The bzr-webdav plugin has been merged into core allowing branches to be
1892+pushed to WebDAV-enabled http servers.
1893+
1894
1895 Further information
1896 *******************