Merge lp:~vila/bzr/webdav-in-core into lp:bzr
- webdav-in-core
- Merge into bzr.dev
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jelmer Vernooij (community) | Needs Information | ||
Review via email: mp+99371@code.launchpad.net |
Commit message
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.
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 ;)
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.
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.
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.
It's a bit surprising to see _raise_
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.
Yes, most of the testing comes from that, only webdav-specific parts are in the plugin.
>
> It's a bit surprising to see _raise_
> 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_
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
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 | ******************* |
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. :-)