bzr-webdav_1.12.2~bzr66.orig/COPYING.txt0000644000000000000000000004310311575443631015762 0ustar 00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. bzr-webdav_1.12.2~bzr66.orig/NOTES0000644000000000000000000000216111575443631014723 0ustar 00000000000000Performances: - Without any optimizations: time bzr push http+webdav://:@/pub/bzr.dev 1948 revision(s) pushed. real 9m54.487s user 1m15.280s sys 0m20.060s The above measure is imprecise and certainly out-of-date. Tests: Installation example: Alias /bzr /srv/DAV DAV On # DirectorySlash tells apache to reply with redirections if # directories miss their final '/'. It does not play well with # bzr (to they the least) and provide no benefits in our # case. So just turn it off. DirectorySlash Off # We need to activate the following which is off by # default. For good security reasons which don't apply to # bzr directories ;) DavDepthInfinity on # The simplest auth scheme is basic, just given as an # example, using https is recommanded with it, or at # least digest if https is not possible. AuthType Basic AuthName bzr AuthUserFile /etc/apache2/dav.users # Write access requires authentication Require valid-user bzr-webdav_1.12.2~bzr66.orig/TODO0000644000000000000000000000366511575443631014612 0ustar 00000000000000* webdav.py ** We can detect that the server do not accept "write" operations (it will return 501) and raise InvalidHttpRequest(to be defined as a daughter of InvalidHttpResponse) but what will the upper layers do ? ** 20060908 All *_file functions are defined in terms of *_bytes because we have to read the file to create a proper PUT request. Is it possible to define PUT with a file-like object, so that we don't have to potentially read in and hold onto potentially 600MB of file contents? ** Factor out the error handling. Try to use Transport.translate_error if it becomes an accessible function. Otherwise duplicate it here (bad) * tests ** Implement the testing of the range header for PUT requests (GET request are already heavily tested in bzr). Test servers are available there too. This will also help for reporting bugs against lighttp. ** Turning directory indexes off may make the server reports that an existing directory does not exist. Reportedly, using multiviews can provoke that too. Investigate and fix. ** A DAV web server can't handle mode on files because: - there is nothing in the protocol for that (bar some of them via PROPPATCH, but only for apache2 anyway), - the server itself generally uses the mode for its own purposes, except if you make it run suid which is really, really dangerous (Apache should be compiled with -D-DBIG_SECURITY_HOLE for those who didn't get the message). That means this transport will do no better. May be the file mode should be a file property handled explicitely inside the repositories and applied by bzr in the working trees. That implies a mean to store file properties, apply them, detecting their changes, etc. It may be possible to use PROPPATCH to handle mode bits, but bzr doesn't try to handle remote working trees. So until the neeed arises, this will remain as is. bzr-webdav_1.12.2~bzr66.orig/__init__.py0000644000000000000000000000405011575443631016220 0ustar 00000000000000# Copyright (C) 2006-2009, 2011 Canonical Ltd # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """An http transport, using webdav to allow pushing. This defines the HttpWebDAV transport, which implement the necessary handling of WebDAV to allow pushing on an http server. """ __version__ = '1.12.2' version_info = tuple(int(n) for n in __version__.split('.')) import bzrlib # Don't go further if we are not compatible if bzrlib.version_info < (1, 12): # We need bzr 1.12 from bzrlib import trace trace.note('not installing http[s]+webdav:// support' ' (only supported for bzr 1.12 and above)') else: from bzrlib import transport transport.register_urlparse_netloc_protocol('http+webdav') transport.register_urlparse_netloc_protocol('https+webdav') transport.register_lazy_transport('https+webdav://', 'bzrlib.plugins.webdav.webdav', 'HttpDavTransport') transport.register_lazy_transport('http+webdav://', 'bzrlib.plugins.webdav.webdav', 'HttpDavTransport') def load_tests(basic_tests, module, loader): testmod_names = [ 'tests', ] basic_tests.addTest(loader.loadTestsFromModuleNames( ["%s.%s" % (__name__, tmn) for tmn in testmod_names])) return basic_tests bzr-webdav_1.12.2~bzr66.orig/setup.py0000644000000000000000000000246111575443631015625 0ustar 00000000000000#!/usr/bin/env python # Copyright (C) 2008 by Canonical Ltd # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from distutils.core import setup setup(name='bzr-webdav', version='1.6.0', maintainer = "vila", description='Allows bzr to push on DAV enabled web servers', keywords='plugin bzr webdav DAV http https', url='http://launchpad.net/bzr.webdav', download_url='http://launchpad.net/bzr.webdav', license='GNU GPL v2 or later', package_dir={'bzrlib.plugins.webdav':'.', 'bzrlib.plugins.webdav.tests':'tests'}, packages=['bzrlib.plugins.webdav', 'bzrlib.plugins.webdav.tests'] ) bzr-webdav_1.12.2~bzr66.orig/tests/0000755000000000000000000000000011575443631015252 5ustar 00000000000000bzr-webdav_1.12.2~bzr66.orig/tests/__init__.py0000644000000000000000000000173411575443631017370 0ustar 00000000000000# Copyright (C) 2008 by Canonical Ltd # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def load_tests(basic_tests, module, loader): testmod_names = [ 'test_webdav', ] basic_tests.addTest(loader.loadTestsFromModuleNames( ["%s.%s" % (__name__, tmn) for tmn in testmod_names])) return basic_tests bzr-webdav_1.12.2~bzr66.orig/tests/dav_server.py0000644000000000000000000003730711575443631017776 0ustar 00000000000000# Copyright (C) 2008 Canonical Ltd # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """DAV test server. This defines the TestingDAVRequestHandler and the DAVServer classes which implements the DAV specification parts used by the webdav plugin. """ import errno import os import os.path # FIXME: Can't we use bzrlib.osutils ? import re import shutil # FIXME: Can't we use bzrlib.osutils ? import stat import time import urlparse # FIXME: Can't we use bzrlib.urlutils ? from bzrlib import ( tests, trace, urlutils, ) from bzrlib.tests import http_server class TestingDAVRequestHandler(http_server.TestingHTTPRequestHandler): """ Subclass of TestingHTTPRequestHandler handling DAV requests. This is not a full implementation of a DAV server, only the parts really used by the plugin are. """ _RANGE_HEADER_RE = re.compile( r'bytes (?P\d+)-(?P\d+)/(?P\d+|\*)') def date_time_string(self, timestamp=None): """Return the current date and time formatted for a message header.""" if timestamp is None: timestamp = time.time() year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp) s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( self.weekdayname[wd], day, self.monthname[month], year, hh, mm, ss) return s def _read(self, length): """Read the client socket""" return self.rfile.read(length) def _readline(self): """Read a full line on the client socket""" return self.rfile.readline() def read_body(self): """Read the body either by chunk or as a whole.""" content_length = self.headers.get('Content-Length') encoding = self.headers.get('Transfer-Encoding') if encoding is not None: assert encoding == 'chunked' body = [] # We receive the content by chunk while True: length, data = self.read_chunk() if length == 0: break body.append(data) body = ''.join(body) else: if content_length is not None: body = self._read(int(content_length)) return body def read_chunk(self): """Read a chunk of data. A chunk consists of: - a line containing the length of the data in hexa, - the data. - a empty line. An empty chunk specifies a length of zero """ length = int(self._readline(),16) data = None if length != 0: data = self._read(length) # Eats the newline following the chunk self._readline() return length, data def send_head(self): """Specialized version of SimpleHttpServer. We *don't* want the apache behavior of permanently redirecting directories without trailing slashes to directories with trailing slashes. That's a waste and a severe penalty for clients with high latency. The installation documentation of the plugin should mention the DirectorySlash apache directive and insists on turning it *Off*. """ path = self.translate_path(self.path) f = None if os.path.isdir(path): for index in "index.html", "index.htm": index = os.path.join(path, index) if os.path.exists(index): path = index break else: return self.list_directory(path) ctype = self.guess_type(path) if ctype.startswith('text/'): mode = 'r' else: mode = 'rb' try: f = open(path, mode) except IOError: self.send_error(404, "File not found") return None self.send_response(200) self.send_header("Content-type", ctype) fs = os.fstat(f.fileno()) self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) self.end_headers() return f def do_PUT(self): """Serve a PUT request.""" # FIXME: test_put_file_unicode makes us emit a traceback because a # UnicodeEncodeError occurs after the request headers have been sent # but before the body can be send. It's harmless and does not make the # test fails. Adressing that will mean protecting all reads from the # socket, which is too heavy for now -- vila 20070917 path = self.translate_path(self.path) trace.mutter("do_PUT rel: [%s], abs: [%s]" % (self.path, path)) do_append = False # Check the Content-Range header range_header = self.headers.get('Content-Range') if range_header is not None: match = self._RANGE_HEADER_RE.match(range_header) if match is None: # FIXME: RFC2616 says to return a 501 if we don't # understand the Content-Range header, but Apache # just ignores them (bad Apache). self.send_error(501, 'Not Implemented') return begin = int(match.group('begin')) do_append = True if self.headers.get('Expect') == '100-continue': # Tell the client to go ahead, we're ready to get the content self.send_response(100,"Continue") self.end_headers() try: trace.mutter("do_PUT will try to open: [%s]" % path) # Always write in binary mode. if do_append: f = open(path,'ab') f.seek(begin) else: f = open(path, 'wb') except (IOError, OSError), e : trace.mutter("do_PUT got: [%r] while opening/seeking on [%s]" % (e, self.path)) self.send_error(409, 'Conflict') return try: data = self.read_body() f.write(data) except (IOError, OSError): # FIXME: We leave a partially written file here self.send_error(409, "Conflict") f.close() return f.close() trace.mutter("do_PUT done: [%s]" % self.path) self.send_response(201) self.end_headers() def do_MKCOL(self): """ Serve a MKCOL request. MKCOL is an mkdir in DAV terminology for our part. """ path = self.translate_path(self.path) trace.mutter("do_MKCOL rel: [%s], abs: [%s]" % (self.path,path)) try: os.mkdir(path) except (IOError, OSError),e: if e.errno in (errno.ENOENT, ): self.send_error(409, "Conflict") elif e.errno in (errno.EEXIST, errno.ENOTDIR): self.send_error(405, "Not allowed") else: # Ok we fail for an unnkown reason :-/ raise else: self.send_response(201) self.end_headers() def do_COPY(self): """Serve a COPY request.""" url_to = self.headers.get('Destination') if url_to is None: self.send_error(400,"Destination header missing") return (scheme, netloc, rel_to, params, query, fragment) = urlparse.urlparse(url_to) trace.mutter("urlparse: (%s) [%s]" % (url_to, rel_to)) trace.mutter("do_COPY rel_from: [%s], rel_to: [%s]" % (self.path, rel_to)) abs_from = self.translate_path(self.path) abs_to = self.translate_path(rel_to) try: # TODO: Check that rel_from exists and rel_to does # not. In the mean time, just go along and trap # exceptions shutil.copyfile(abs_from,abs_to) except (IOError, OSError), e: if e.errno == errno.ENOENT: self.send_error(404,"File not found") ; else: self.send_error(409,"Conflict") ; else: # TODO: We may be able to return 204 "No content" if # rel_to was existing (even if the "No content" part # seems misleading, RFC2518 says so, stop arguing :) self.send_response(201) self.end_headers() def do_DELETE(self): """Serve a DELETE request. We don't implement a true DELETE as DAV defines it because we *should* fail to delete a non empty dir. """ path = self.translate_path(self.path) trace.mutter("do_DELETE rel: [%s], abs: [%s]" % (self.path, path)) try: # DAV makes no distinction between files and dirs # when required to nuke them, but we have to. And we # also watch out for symlinks. real_path = os.path.realpath(path) if os.path.isdir(real_path): os.rmdir(path) else: os.remove(path) except (IOError, OSError),e: if e.errno in (errno.ENOENT, ): self.send_error(404, "File not found") else: # Ok we fail for an unnkown reason :-/ raise else: self.send_response(204) # Default success code self.end_headers() def do_MOVE(self): """Serve a MOVE request.""" url_to = self.headers.get('Destination') if url_to is None: self.send_error(400, "Destination header missing") return overwrite_header = self.headers.get('Overwrite') if overwrite_header == 'F': should_overwrite = False else: should_overwrite = True (scheme, netloc, rel_to, params, query, fragment) = urlparse.urlparse(url_to) trace.mutter("urlparse: (%s) [%s]" % (url_to, rel_to)) trace.mutter("do_MOVE rel_from: [%s], rel_to: [%s]" % (self.path, rel_to)) abs_from = self.translate_path(self.path) abs_to = self.translate_path(rel_to) if should_overwrite is False and os.access(abs_to, os.F_OK): self.send_error(412, "Precondition Failed") return try: os.rename(abs_from, abs_to) except (IOError, OSError), e: if e.errno == errno.ENOENT: self.send_error(404, "File not found") ; else: self.send_error(409, "Conflict") ; else: # TODO: We may be able to return 204 "No content" if # rel_to was existing (even if the "No content" part # seems misleading, RFC2518 says so, stop arguing :) self.send_response(201) self.end_headers() def _generate_response(self, path): local_path = self.translate_path(path) st = os.stat(local_path) prop = dict() def _prop(ns, name, value=None): if value is None: return '<%s:%s/>' % (ns, name) else: return '<%s:%s>%s' % (ns, name, value, ns, name) # For namespaces (and test purposes), where apache2 use: # - lp1, we use liveprop, # - lp2, we use bzr if stat.S_ISDIR(st.st_mode): dpath = path if not dpath.endswith('/'): dpath += '/' prop['href'] = _prop('D', 'href', dpath) prop['type'] = _prop('liveprop', 'resourcetype', '') prop['length'] = '' prop['exec'] = '' else: # FIXME: assert S_ISREG ? Handle symlinks ? prop['href'] = _prop('D', 'href', path) prop['type'] = _prop('liveprop', 'resourcetype') prop['length'] = _prop('liveprop', 'getcontentlength', st.st_size) if st.st_mode & stat.S_IXUSR: is_exec = 'T' else: is_exec = 'F' prop['exec'] = _prop('bzr', 'excutable', is_exec) prop['status'] = _prop('D', 'status', 'HTTP/1.1 200 OK') response = """ %(href)s %(type)s %(length)s %(exec)s %(status)s """ % prop return response, st def _generate_dir_responses(self, path, depth): local_path = self.translate_path(path) entries = os.listdir(local_path) for entry in entries: entry_path = urlutils.escape(entry) if path.endswith('/'): entry_path = path + entry_path else: entry_path = path + '/' + entry_path response, st = self._generate_response(entry_path) yield response if depth == 'Infinity' and stat.S_ISDIR(st.st_mode): for sub_resp in self._generate_dir_responses(entry_path, depth): yield sub_resp def do_PROPFIND(self): """Serve a PROPFIND request.""" depth = self.headers.get('Depth') if depth is None: depth = 'Infinity' if depth not in ('0', '1', 'Infinity'): self.send_error(400, "Bad Depth") return path = self.translate_path(self.path) # Don't bother parsing the body, we handle only allprop anyway. # FIXME: Handle the body :) data = self.read_body() try: response, st = self._generate_response(self.path) except OSError, e: if e.errno == errno.ENOENT: self.send_error(404) return else: raise if depth in ('1', 'Infinity') and stat.S_ISDIR(st.st_mode): dir_responses = self._generate_dir_responses(self.path, depth) else: dir_responses = [] # Generate the response, we don't care about performance, so we just # expand everything into a big string. response = """ %s%s """ % (response, ''.join(list(dir_responses))) self.send_response(207) self.send_header('Content-length', len(response)) self.end_headers() self.wfile.write(response) class DAVServer(http_server.HttpServer): """Subclass of HttpServer that gives http+webdav urls. This is for use in testing: connections to this server will always go through _urllib where possible. """ def __init__(self): # We have special requests to handle that # HttpServer_urllib doesn't know about super(DAVServer,self).__init__(TestingDAVRequestHandler) # urls returned by this server should require the webdav client impl _url_protocol = 'http+webdav' class TestCaseWithDAVServer(tests.TestCaseWithTransport): """A support class that provides urls that are http+webdav://. This is done by forcing the server to be an http DAV one. """ def setUp(self): super(TestCaseWithDAVServer, self).setUp() self.transport_server = DAVServer bzr-webdav_1.12.2~bzr66.orig/tests/test_webdav.py0000644000000000000000000003315011575443631020135 0ustar 00000000000000# Copyright (C) 2008 Canonical Ltd # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tests for the wedav plugin.""" from cStringIO import StringIO import stat from bzrlib import ( errors, tests, ) from bzrlib.plugins.webdav import webdav def _get_list_dir_apache2_depth_1_prop(): return """ /19016477731212686926.835527/ HTTP/1.1 200 OK /19016477731212686926.835527/a HTTP/1.1 200 OK /19016477731212686926.835527/b HTTP/1.1 200 OK /19016477731212686926.835527/c/ HTTP/1.1 200 OK """ def _get_list_dir_apache2_depth_1_allprop(): return """ / 2008-06-08T10:50:38Z Sun, 08 Jun 2008 10:50:38 GMT "da7f5a-cc-7722db80" HTTP/1.1 200 OK /executable 2008-06-08T09:50:15Z 14 Sun, 08 Jun 2008 09:50:11 GMT "da9f81-0-9ef33ac0" T HTTP/1.1 200 OK /read-only 2008-06-08T09:50:11Z 42 Sun, 08 Jun 2008 09:50:11 GMT "da9f80-0-9ef33ac0" F HTTP/1.1 200 OK /titi 2008-06-08T09:49:53Z 6 Sun, 08 Jun 2008 09:49:53 GMT "da8cbc-6-9de09240" F HTTP/1.1 200 OK /toto/ 2008-06-06T08:07:07Z Fri, 06 Jun 2008 08:07:07 GMT "da8cb9-44-f2ac20c0" HTTP/1.1 200 OK """ class TestDavSaxParser(tests.TestCase): def _extract_dir_content_from_str(self, str): return webdav._extract_dir_content( 'http://localhost/blah', StringIO(str)) def _extract_stat_from_str(self, str): return webdav._extract_stat_info( 'http://localhost/blah', StringIO(str)) def test_unkown_format_response(self): # Valid but unrelated xml example = """""" self.assertRaises(errors.InvalidHttpResponse, self._extract_dir_content_from_str, example) def test_list_dir_malformed_response(self): # Invalid xml, neither multistatus nor response are properly closed example = """ http://localhost/""" self.assertRaises(errors.InvalidHttpResponse, self._extract_dir_content_from_str, example) def test_list_dir_incomplete_format_response(self): # The information we need is not present example = """ http://localhost/ http://localhost/titi http://localhost/toto """ self.assertRaises(errors.NotADirectory, self._extract_dir_content_from_str, example) def test_list_dir_apache2_example(self): example = _get_list_dir_apache2_depth_1_prop() self.assertRaises(errors.NotADirectory, self._extract_dir_content_from_str, example) def test_list_dir_lighttpd_example(self): example = """ http://localhost/ http://localhost/titi http://localhost/toto """ self.assertRaises(errors.NotADirectory, self._extract_dir_content_from_str, example) def test_list_dir_apache2_dir_depth_1_example(self): example = _get_list_dir_apache2_depth_1_allprop() self.assertEquals([('executable', False, 14, True), ('read-only', False, 42, False), ('titi', False, 6, False), ('toto', True, -1, False)], self._extract_dir_content_from_str(example)) def test_stat_malformed_response(self): # Invalid xml, neither multistatus nor response are properly closed example = """ http://localhost/""" self.assertRaises(errors.InvalidHttpResponse, self._extract_stat_from_str, example) def test_stat_incomplete_format_response(self): # The minimal information is present but doesn't conform to RFC 2518 # (well, as I understand it since the reference servers disagree on # more than details). # The href below is not enclosed in a response element and is # therefore ignored. example = """ http://localhost/toto """ self.assertRaises(errors.InvalidHttpResponse, self._extract_stat_from_str, example) def test_stat_apache2_file_example(self): example = """ /executable 2008-06-08T09:50:15Z 12 Sun, 08 Jun 2008 09:50:11 GMT "da9f81-0-9ef33ac0" T HTTP/1.1 200 OK """ st = self._extract_stat_from_str(example) self.assertEquals(12, st.st_size) self.assertFalse(stat.S_ISDIR(st.st_mode)) self.assertTrue(stat.S_ISREG(st.st_mode)) self.assertTrue(st.st_mode & stat.S_IXUSR) def test_stat_apache2_dir_depth_1_example(self): example = _get_list_dir_apache2_depth_1_allprop() self.assertRaises(errors.InvalidHttpResponse, self._extract_stat_from_str, example) def test_stat_apache2_dir_depth_0_example(self): example = """ / 2008-06-08T10:50:38Z Sun, 08 Jun 2008 10:50:38 GMT "da7f5a-cc-7722db80" HTTP/1.1 200 OK """ st = self._extract_stat_from_str(example) self.assertEquals(-1, st.st_size) self.assertTrue(stat.S_ISDIR(st.st_mode)) self.assertTrue(st.st_mode & stat.S_IXUSR) bzr-webdav_1.12.2~bzr66.orig/webdav.py0000644000000000000000000007625511575443631015751 0ustar 00000000000000# Copyright (C) 2006-2009, 2011 Canonical Ltd # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Implementation of WebDAV for http transports. A Transport which complement http transport by implementing partially the WebDAV protocol to push files. This should enable remote push operations. """ from cStringIO import StringIO import os import random import re import sys import time import urllib2 import xml.sax import xml.sax.handler from bzrlib import ( errors, osutils, trace, transport, urlutils, ) from bzrlib.transport.http import ( _urllib, _urllib2_wrappers, ) class DavResponseHandler(xml.sax.handler.ContentHandler): """Handle a multi-status DAV response.""" def __init__(self): self.url = None self.elt_stack = None self.chars = None self.chars_wanted = False self.expected_content_handled = False def set_url(self, url): """Set the url used for error reporting when handling a response.""" self.url = url def startDocument(self): self.elt_stack = [] self.chars = None self.expected_content_handled = False def endDocument(self): self._validate_handling() if not self.expected_content_handled: raise errors.InvalidHttpResponse(self.url, msg='Unknown xml response') def startElement(self, name, attrs): self.elt_stack.append(self._strip_ns(name)) # The following is incorrect in the general case where elements are # intermixed with chars in a higher level element. That's not the case # here (otherwise the chars_wanted will have to be stacked too). if self.chars_wanted: self.chars = '' else: self.chars = None def endElement(self, name): self.chars = None self.chars_wanted = False self.elt_stack.pop() def characters(self, chrs): if self.chars_wanted: self.chars += chrs def _current_element(self): return self.elt_stack[-1] def _strip_ns(self, name): """Strip the leading namespace from name. We don't have namespaces clashes in our context, stripping it makes the code simpler. """ where = name.find(':') if where == -1: return name else: return name[where +1:] class DavStatHandler(DavResponseHandler): """Handle a PROPPFIND DAV response for a file or directory. The expected content is: - a multi-status element containing - a single response element containing - a href element - a propstat element containing - a status element (ignored) - a prop element containing at least (other are ignored) - a getcontentlength element (for files only) - an executable element (for files only) - a resourcetype element containing - a collection element (for directories only) """ def __init__(self): DavResponseHandler.__init__(self) # Flags defining the context for the actions self._response_seen = False self._init_response_attrs() def _init_response_attrs(self): self.href = None self.length = -1 self.executable = None self.is_dir = False def _validate_handling(self): if self.href is not None: self.expected_content_handled = True def startElement(self, name, attrs): sname = self._strip_ns(name) self.chars_wanted = sname in ('href', 'getcontentlength', 'executable') DavResponseHandler.startElement(self, name, attrs) def endElement(self, name): if self._response_seen: self._additional_response_starting(name) if self._href_end(): self.href = self.chars elif self._getcontentlength_end(): self.length = int(self.chars) elif self._executable_end(): self.executable = self.chars elif self._collection_end(): self.is_dir = True if self._strip_ns(name) == 'response': self._response_seen = True self._response_handled() DavResponseHandler.endElement(self, name) def _response_handled(self): """A response element inside a multistatus have been parsed.""" pass def _additional_response_starting(self, name): """A additional response element inside a multistatus begins.""" sname = self._strip_ns(name) if sname != 'multistatus': raise errors.InvalidHttpResponse( self.url, msg='Unexpected %s element' % name) def _href_end(self): stack = self.elt_stack return (len(stack) == 3 and stack[0] == 'multistatus' and stack[1] == 'response' and stack[2] == 'href') def _getcontentlength_end(self): stack = self.elt_stack return (len(stack) == 5 and stack[0] == 'multistatus' and stack[1] == 'response' and stack[2] == 'propstat' and stack[3] == 'prop' and stack[4] == 'getcontentlength') def _executable_end(self): stack = self.elt_stack return (len(stack) == 5 and stack[0] == 'multistatus' and stack[1] == 'response' and stack[2] == 'propstat' and stack[3] == 'prop' and stack[4] == 'executable') def _collection_end(self): stack = self.elt_stack return (len(stack) == 6 and stack[0] == 'multistatus' and stack[1] == 'response' and stack[2] == 'propstat' and stack[3] == 'prop' and stack[4] == 'resourcetype' and stack[5] == 'collection') class _DAVStat(object): """The stat info as it can be acquired with DAV.""" def __init__(self, size, is_dir, is_exec): self.st_size = size # We build a mode considering that: # - we have no idea about group or other chmod bits so we use a sane # default (bzr should not care anyway) # - we suppose that the user can write if is_dir: self.st_mode = 0040644 else: self.st_mode = 0100644 if is_exec: self.st_mode = self.st_mode | 0755 def _extract_stat_info(url, infile): """Extract the stat-like information from a DAV PROPFIND response. :param url: The url used for the PROPFIND request. :param infile: A file-like object pointing at the start of the response. """ parser = xml.sax.make_parser() handler = DavStatHandler() handler.set_url(url) parser.setContentHandler(handler) try: parser.parse(infile) except xml.sax.SAXParseException, e: raise errors.InvalidHttpResponse( url, msg='Malformed xml response: %s' % e) if handler.is_dir: size = -1 # directory sizes are meaningless for bzr is_exec = True else: size = handler.length is_exec = (handler.executable == 'T') return _DAVStat(size, handler.is_dir, is_exec) class DavListDirHandler(DavStatHandler): """Handle a PROPPFIND depth 1 DAV response for a directory.""" def __init__(self): DavStatHandler.__init__(self) self.dir_content = None def _validate_handling(self): if self.dir_content is not None: self.expected_content_handled = True def _make_response_tuple(self): if self.executable == 'T': is_exec = True else: is_exec = False return (self.href, self.is_dir, self.length, is_exec) def _response_handled(self): """A response element inside a multistatus have been parsed.""" if self.dir_content is None: self.dir_content = [] self.dir_content.append(self._make_response_tuple()) # Resest the attributes for the next response if any self._init_response_attrs() def _additional_response_starting(self, name): """A additional response element inside a multistatus begins.""" pass def _extract_dir_content(url, infile): """Extract the directory content from a DAV PROPFIND response. :param url: The url used for the PROPFIND request. :param infile: A file-like object pointing at the start of the response. """ parser = xml.sax.make_parser() handler = DavListDirHandler() handler.set_url(url) parser.setContentHandler(handler) try: parser.parse(infile) except xml.sax.SAXParseException, e: raise errors.InvalidHttpResponse( url, msg='Malformed xml response: %s' % e) # Reformat for bzr needs dir_content = handler.dir_content (dir_name, is_dir) = dir_content[0][:2] if not is_dir: raise errors.NotADirectory(url) dir_len = len(dir_name) elements = [] for (href, is_dir, size, is_exec) in dir_content[1:]: # Ignore first element if href.startswith(dir_name): name = href[dir_len:] if name.endswith('/'): # Get rid of final '/' name = name[0:-1] # We receive already url-encoded strings so down-casting is # safe. And bzr insists on getting strings not unicode strings. elements.append((str(name), is_dir, size, is_exec)) return elements class PUTRequest(_urllib2_wrappers.Request): def __init__(self, url, data, more_headers={}, accepted_errors=None): # FIXME: Accept */* ? Why ? *we* send, we do not receive :-/ headers = {'Accept': '*/*', 'Content-type': 'application/octet-stream', # FIXME: We should complete the # implementation of # htmllib.HTTPConnection, it's just a # shame (at least a waste) that we # can't use the following. # 'Expect': '100-continue', # 'Transfer-Encoding': 'chunked', } headers.update(more_headers) _urllib2_wrappers.Request.__init__(self, 'PUT', url, data, headers, accepted_errors=accepted_errors) class DavResponse(_urllib2_wrappers.Response): """Custom HTTPResponse. DAV have some reponses for which the body is of no interest. """ _body_ignored_responses = ( _urllib2_wrappers.Response._body_ignored_responses + [201, 405, 409, 412,] ) def begin(self): """Begin to read the response from the server. httplib incorrectly close the connection far too easily. Let's try to workaround that (as _urllib2 does, but for more cases...). """ _urllib2_wrappers.Response.begin(self) if self.status in (201, 204): self.will_close = False # Takes DavResponse into account: class DavHTTPConnection(_urllib2_wrappers.HTTPConnection): response_class = DavResponse class DavHTTPSConnection(_urllib2_wrappers.HTTPSConnection): response_class = DavResponse class DavConnectionHandler(_urllib2_wrappers.ConnectionHandler): """Custom connection handler. We need to use the DavConnectionHTTPxConnection class to take into account our own DavResponse objects, to be able to declare our own body ignored responses, sigh. """ def http_request(self, request): return self.capture_connection(request, DavHTTPConnection) def https_request(self, request): return self.capture_connection(request, DavHTTPSConnection) class DavOpener(_urllib2_wrappers.Opener): """Dav specific needs regarding HTTP(S)""" def __init__(self, report_activity=None): super(DavOpener, self).__init__(connection=DavConnectionHandler, report_activity=report_activity) class HttpDavTransport(_urllib.HttpTransport_urllib): """An transport able to put files using http[s] on a DAV server. We don't try to implement the whole WebDAV protocol. Just the minimum needed for bzr. """ _debuglevel = 0 _opener_class = DavOpener def is_readonly(self): """See Transport.is_readonly.""" return False def _raise_http_error(self, url, response, info=None): if info is None: msg = '' else: msg = ': ' + info raise errors.InvalidHttpResponse(url, 'Unable to handle http code %d%s' % (response.code, msg)) def _handle_common_errors(self, code, abspath): if code == 404: raise errors.NoSuchFile(abspath) def open_write_stream(self, relpath, mode=None): """See Transport.open_write_stream.""" # FIXME: this implementation sucks, we should really use chunk encoding # and buffers. self.put_bytes(relpath, "", mode) result = transport.AppendBasedFileStream(self, relpath) transport._file_streams[self.abspath(relpath)] = result return result def put_file(self, relpath, f, mode=None): """See Transport.put_file""" # FIXME: We read the whole file in memory, using chunked encoding and # counting bytes while sending them will be far better. Look at reusing # osutils.pumpfile ? # bytes = f.read() self.put_bytes(relpath, bytes, mode=None) return len(bytes) def put_bytes(self, relpath, bytes, mode=None): """Copy the bytes object into the location. Tests revealed that contrary to what is said in http://www.rfc.net/rfc2068.html, the put is not atomic. When putting a file, if the client died, a partial file may still exists on the server. So we first put a temp file and then move it. :param relpath: Location to put the contents, relative to base. :param f: File-like object. :param mode: Not supported by DAV. """ abspath = self._remote_path(relpath) # We generate a sufficiently random name to *assume* that # no collisions will occur and don't worry about it (nor # handle it). stamp = '.tmp.%.9f.%d.%d' % (time.time(), os.getpid(), random.randint(0,0x7FFFFFFF)) # A temporary file to hold all the data to guard against # client death tmp_relpath = relpath + stamp # Will raise if something gets wrong self.put_bytes_non_atomic(tmp_relpath, bytes) # Now move the temp file try: self.move(tmp_relpath, relpath) except Exception, e: # If we fail, try to clean up the temporary file # before we throw the exception but don't let another # exception mess things up. exc_type, exc_val, exc_tb = sys.exc_info() try: self.delete(tmp_relpath) except: raise exc_type, exc_val, exc_tb raise # raise the original with its traceback if we can. def put_file_non_atomic(self, relpath, f, mode=None, create_parent_dir=False, dir_mode=False): # Implementing put_bytes_non_atomic rather than put_file_non_atomic # because to do a put request, we must read all of the file into # RAM anyway. Better to do that than to have the contents, put # into a StringIO() and then read them all out again later. self.put_bytes_non_atomic(relpath, f.read(), mode=mode, create_parent_dir=create_parent_dir, dir_mode=dir_mode) def put_bytes_non_atomic(self, relpath, bytes, mode=None, create_parent_dir=False, dir_mode=False): """See Transport.put_file_non_atomic""" abspath = self._remote_path(relpath) request = PUTRequest(abspath, bytes, accepted_errors=[200, 201, 204, 403, 404, 409]) def bare_put_file_non_atomic(): response = self._perform(request) code = response.code if code in (403, 404, 409): # Intermediate directories missing raise errors.NoSuchFile(abspath) if code not in (200, 201, 204): self._raise_curl_http_error(abspath, response, 'expected 200, 201 or 204.') try: bare_put_file_non_atomic() except errors.NoSuchFile: if not create_parent_dir: raise parent_dir = osutils.dirname(relpath) if parent_dir: self.mkdir(parent_dir, mode=dir_mode) return bare_put_file_non_atomic() else: # Don't forget to re-raise if the parent dir doesn't exist raise def _put_bytes_ranged(self, relpath, bytes, at): """Append the file-like object part to the end of the location. :param relpath: Location to put the contents, relative to base. :param bytes: A string of bytes to upload :param at: The position in the file to add the bytes """ # Acquire just the needed data # TODO: jam 20060908 Why are we creating a StringIO to hold the # data, and then using data.read() to send the data # in the PUTRequest. Rather than just reading in and # uploading the data. # Also, if we have to read the whole file into memory anyway # it would be better to implement put_bytes(), and redefine # put_file as self.put_bytes(relpath, f.read()) # Once we teach httplib to do that, we will use file-like # objects (see handling chunked data and 100-continue). abspath = self._remote_path(relpath) # Content-Range is start-end/size. 'size' is the file size, not the # chunk size. We can't be sure about the size of the file so put '*' at # the end of the range instead. request = PUTRequest(abspath, bytes, {'Content-Range': 'bytes %d-%d/*' % (at, at+len(bytes)),}, accepted_errors=[200, 201, 204, 403, 404, 409]) response = self._perform(request) code = response.code if code in (403, 404, 409): raise errors.NoSuchFile(abspath) # Intermediate directories missing if code not in (200, 201, 204): self._raise_http_error(abspath, response, 'expected 200, 201 or 204.') def mkdir(self, relpath, mode=None): """See Transport.mkdir""" abspath = self._remote_path(relpath) request = _urllib2_wrappers.Request('MKCOL', abspath, accepted_errors=[201, 403, 405, 404, 409]) response = self._perform(request) code = response.code # jam 20060908: The error handling seems to be repeated for # each function. Is it possible to factor it out into # a helper rather than repeat it for each one? # (I realize there is some custom behavior) # Yes it is and will be done. if code == 403: # Forbidden (generally server misconfigured or not # configured for DAV) raise self._raise_http_error(abspath, response, 'mkdir failed') elif code == 405: # Not allowed (generally already exists) raise errors.FileExists(abspath) elif code in (404, 409): # Conflict (intermediate directories do not exist) raise errors.NoSuchFile(abspath) elif code != 201: # Created raise self._raise_http_error(abspath, response, 'mkdir failed') def rename(self, rel_from, rel_to): """Rename without special overwriting""" abs_from = self._remote_path(rel_from) abs_to = self._remote_path(rel_to) request = _urllib2_wrappers.Request('MOVE', abs_from, None, {'Destination': abs_to, 'Overwrite': 'F'}, accepted_errors=[201, 404, 409, 412]) response = self._perform(request) code = response.code if code == 404: raise errors.NoSuchFile(abs_from) if code == 412: raise errors.FileExists(abs_to) if code == 409: # More precisely some intermediate directories are missing raise errors.NoSuchFile(abs_to) if code != 201: # As we don't want to accept overwriting abs_to, 204 # (meaning abs_to was existing (but empty, the # non-empty case is 412)) will be an error, a server # bug even, since we require explicitely to not # overwrite. self._raise_http_error(abs_from, response, 'unable to rename to %r' % (abs_to)) def move(self, rel_from, rel_to): """See Transport.move""" abs_from = self._remote_path(rel_from) abs_to = self._remote_path(rel_to) request = _urllib2_wrappers.Request('MOVE', abs_from, None, {'Destination': abs_to}, accepted_errors=[201, 204, 404, 409]) response = self._perform(request) code = response.code if code == 404: raise errors.NoSuchFile(abs_from) if code == 409: raise errors.DirectoryNotEmpty(abs_to) # Overwriting allowed, 201 means abs_to did not exist, # 204 means it did exist. if code not in (201, 204): self._raise_http_error(abs_from, response, 'unable to move to %r' % (abs_to)) def delete(self, rel_path): """ Delete the item at relpath. Note that when a non-empty dir required to be deleted, a conforming DAV server will delete the dir and all its content. That does not normally happen in bzr. """ abs_path = self._remote_path(rel_path) request = _urllib2_wrappers.Request('DELETE', abs_path, accepted_errors=[200, 204, 404, 999]) response = self._perform(request) code = response.code if code == 404: raise errors.NoSuchFile(abs_path) if code != 204: self._raise_curl_http_error(curl, 'unable to delete') def copy(self, rel_from, rel_to): """See Transport.copy""" abs_from = self._remote_path(rel_from) abs_to = self._remote_path(rel_to) request = _urllib2_wrappers.Request( 'COPY', abs_from, None, {'Destination': abs_to}, accepted_errors=[201, 204, 404, 409]) response = self._perform(request) code = response.code if code in (404, 409): raise errors.NoSuchFile(abs_from) # XXX: our test server returns 201 but apache2 returns 204, need # investivation. if code not in(201, 204): self._raise_http_error(abs_from, response, 'unable to copy from %r to %r' % (abs_from,abs_to)) def copy_to(self, relpaths, other, mode=None, pb=None): """Copy a set of entries from self into another Transport. :param relpaths: A list/generator of entries to be copied. """ # DavTransport can be a target. So our simple implementation # just returns the Transport implementation. (Which just does # a put(get()) # We only override, because the default HttpTransportBase, explicitly # disabled it for HTTP return transport.Transport.copy_to(self, relpaths, other, mode=mode, pb=pb) def listable(self): """See Transport.listable.""" return True def list_dir(self, relpath): """ Return a list of all files at the given location. """ return [elt[0] for elt in self._list_tree(relpath, 1)] def _list_tree(self, relpath, depth): abspath = self._remote_path(relpath) propfind = """ """ request = _urllib2_wrappers.Request('PROPFIND', abspath, propfind, {'Depth': depth}, accepted_errors=[207, 404, 409,]) response = self._perform(request) code = response.code if code == 404: raise errors.NoSuchFile(abspath) if code == 409: # More precisely some intermediate directories are missing raise errors.NoSuchFile(abspath) if code != 207: self._raise_http_error(abspath, response, 'unable to list %r directory' % (abspath)) return _extract_dir_content(abspath, response) def lock_write(self, relpath): """Lock the given file for exclusive access. :return: A lock object, which should be passed to Transport.unlock() """ # We follow the same path as FTP, which just returns a BogusLock # object. We don't explicitly support locking a specific file. # TODO: jam 2006-09-08 SFTP implements this by opening exclusive # "relpath + '.lock_write'". Does DAV implement anything like # O_EXCL? # Alternatively, LocalTransport uses an OS lock to lock the file # and WebDAV supports some sort of locking. return self.lock_read(relpath) def rmdir(self, relpath): """See Transport.rmdir.""" content = self.list_dir(relpath) if len(content) > 0: raise errors.DirectoryNotEmpty(self._remote_path(relpath)) self.delete(relpath) def stat(self, relpath): """See Transport.stat. We provide a limited implementation for bzr needs. """ abspath = self._remote_path(relpath) propfind = """ """ request = _urllib2_wrappers.Request('PROPFIND', abspath, propfind, {'Depth': '0'}, accepted_errors=[207, 404, 409,]) response = self._perform(request) code = response.code if code == 404: raise errors.NoSuchFile(abspath) if code == 409: # FIXME: Could this really occur ? # More precisely some intermediate directories are missing raise errors.NoSuchFile(abspath) if code != 207: self._raise_http_error(abspath, response, 'unable to list %r directory' % (abspath)) return _extract_stat_info(abspath, response) def iter_files_recursive(self): """Walk the relative paths of all files in this transport.""" # We get the whole tree with a single request tree = self._list_tree('.', 'Infinity') # Now filter out the directories for (name, is_dir, size, is_exex) in tree: if not is_dir: yield name def append_file(self, relpath, f, mode=None): """See Transport.append_file""" return self.append_bytes(relpath, f.read(), mode=mode) def append_bytes(self, relpath, bytes, mode=None): """See Transport.append_bytes""" if self._range_hint is not None: # TODO: We reuse the _range_hint handled by bzr core, # unless someone can show me a server implementing # range for write but not for read. But we may, on # our own, try to handle a similar flag for write # ranges supported by a given server. Or at least, # detect that ranges are not correctly handled and # fallback to no ranges. before = self._append_by_head_put(relpath, bytes) else: before = self._append_by_get_put(relpath, bytes) return before def _append_by_head_put(self, relpath, bytes): """Append without getting the whole file. When the server allows it, a 'Content-Range' header can be specified. """ response = self._head(relpath) code = response.code if code == 404: relpath_size = 0 else: # Consider the absence of Content-Length header as # indicating an existing but empty file (Apache 2.0 # does this, and there is even a comment in # modules/http/http_protocol.c calling that a *hack*, # I agree, it's a hack. On the other hand if the file # do not exist we get a 404, if the file does exist, # is not empty and we get no Content-Length header, # then the server is buggy :-/ ) relpath_size = int(response.headers.get('Content-Length', 0)) if relpath_size == 0: trace.mutter('if %s is not empty, the server is buggy' % relpath) if relpath_size: self._put_bytes_ranged(relpath, bytes, relpath_size) else: self.put_bytes(relpath, bytes) return relpath_size def _append_by_get_put(self, relpath, bytes): # So we need to GET the file first, append to it and # finally PUT back the result. full_data = StringIO() try: data = self.get(relpath) full_data.write(data.read()) except errors.NoSuchFile: # Good, just do the put then pass # Append the f content before = full_data.tell() full_data.write(bytes) full_data.seek(0) self.put_file(relpath, full_data) return before def get_smart_medium(self): # smart server and webdav are exclusive. There is really no point to # use webdav if a smart server is available raise errors.NoSmartMedium(self) def get_test_permutations(): """Return the permutations to be used in testing.""" import tests.dav_server return [(HttpDavTransport, tests.dav_server.DAVServer),]