1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
29 from pywebdav.lib.constants import COLLECTION # , OBJECT
30 from pywebdav.lib.errors import DAV_Error, DAV_Forbidden, DAV_NotFound
31 from pywebdav.lib.iface import dav_interface
32 from pywebdav.lib.davcmd import copyone, copytree, moveone, movetree, delone, deltree
34 from DAV.constants import COLLECTION #, OBJECT
35 from DAV.errors import DAV_Error, DAV_Forbidden, DAV_NotFound
36 from DAV.iface import dav_interface
37 from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree
40 from openerp import pooler, sql_db, netsvc
41 from openerp.tools import misc
43 from cache import memoize
44 from webdav import mk_lock_response
48 #hack for urlparse: add webdav in the net protocols
49 urlparse.uses_netloc.append('webdav')
50 urlparse.uses_netloc.append('webdavs')
52 day_names = { 0: 'Mon', 1: 'Tue' , 2: 'Wed', 3: 'Thu', 4: 'Fri', 5: 'Sat', 6: 'Sun' }
53 month_names = { 1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
54 7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec' }
56 def dict_merge2(*dicts):
57 """ Return a dict with all values of dicts.
58 If some key appears twice and contains iterable objects, the values
59 are merged (instead of overwritten).
64 if k in res and isinstance(res[k], (list, tuple)):
65 res[k] = res[k] + d[k]
66 elif k in res and isinstance(res[k], dict):
72 class DAV_NotFound2(DAV_NotFound):
73 """404 exception, that accepts our list uris
75 def __init__(self, *args):
76 if len(args) and isinstance(args[0], (tuple, list)):
77 path = ''.join([ '/' + x for x in args[0]])
79 DAV_NotFound.__init__(self, *args)
83 """ Convert a string with time representation (from db) into time (float)
88 if isinstance(cre, basestring) and '.' in cre:
90 frac = float(cre[fdot:])
92 return time.mktime(time.strptime(cre,'%Y-%m-%d %H:%M:%S')) + frac
94 class BoundStream2(object):
95 """Wraps around a seekable buffer, reads a determined range of data
97 Note that the supplied stream object MUST support a size() which
98 should return its data length (in bytes).
100 A variation of the class in websrv_lib.py
103 def __init__(self, stream, offset=None, length=None, chunk_size=None):
104 self._stream = stream
105 self._offset = offset or 0
106 self._length = length or self._stream.size()
107 self._rem_length = length
108 assert length and isinstance(length, (int, long))
109 assert length and length >= 0, length
110 self._chunk_size = chunk_size
111 if offset is not None:
112 self._stream.seek(offset)
114 def read(self, size=-1):
116 raise IOError(errno.EBADF, "read() without stream.")
118 if self._rem_length == 0:
120 elif self._rem_length < 0:
123 rsize = self._rem_length
124 if size > 0 and size < rsize:
126 if self._chunk_size and self._chunk_size < rsize:
127 rsize = self._chunk_size
129 data = self._stream.read(rsize)
130 self._rem_length -= len(data)
138 res = self._stream.tell()
147 return self.read(65536)
149 def seek(self, pos, whence=os.SEEK_SET):
150 """ Seek, computing our limited range
152 if whence == os.SEEK_SET:
153 if pos < 0 or pos > self._length:
154 raise IOError(errno.EINVAL,"Cannot seek.")
155 self._stream.seek(pos - self._offset)
156 self._rem_length = self._length - pos
157 elif whence == os.SEEK_CUR:
159 if pos > self._rem_length:
160 raise IOError(errno.EINVAL,"Cannot seek past end.")
164 raise IOError(errno.EINVAL,"Cannot seek before start.")
165 self._stream.seek(pos, os.SEEK_CUR)
166 self._rem_length -= pos
167 elif whence == os.SEEK_END:
169 raise IOError(errno.EINVAL,"Cannot seek past end.")
171 if self._length + pos < 0:
172 raise IOError(errno.EINVAL,"Cannot seek before start.")
173 newpos = self._offset + self._length + pos
174 self._stream.seek(newpos, os.SEEK_SET)
175 self._rem_length = 0 - pos
177 class openerp_dav_handler(dav_interface):
179 This class models a OpenERP interface for the DAV server
181 PROPS={'DAV:': dav_interface.PROPS['DAV:'],}
183 M_NS={ "DAV:" : dav_interface.M_NS['DAV:'],}
185 def __init__(self, parent, verbose=False):
188 self.baseuri = parent.baseuri
189 self.verbose = verbose
191 def get_propnames(self, uri):
193 self.parent.log_message('get propnames: %s' % uri)
194 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
197 # TODO: maybe limit props for databases..?
199 node = self.uri2object(cr, uid, pool, uri2)
201 props = dict_merge2(props, node.get_dav_props(cr))
205 def _try_function(self, funct, args, opname='run function', cr=None,
206 default_exc=DAV_Forbidden):
207 """ Try to run a function, and properly convert exceptions to DAV ones.
209 @objname the name of the operation being performed
210 @param cr if given, the cursor to close at exceptions
218 except NotImplementedError, e:
221 self.parent.log_error("Cannot %s: %s", opname, str(e))
222 self.parent.log_message("Exc: %s",traceback.format_exc())
223 # see par 9.3.1 of rfc
224 raise DAV_Error(403, str(e) or 'Not supported at this path.')
225 except EnvironmentError, err:
228 self.parent.log_error("Cannot %s: %s", opname, err.strerror)
229 self.parent.log_message("Exc: %s",traceback.format_exc())
230 raise default_exc(err.strerror)
234 self.parent.log_error("Cannot %s: %s", opname, str(e))
235 self.parent.log_message("Exc: %s",traceback.format_exc())
236 raise default_exc("Operation failed.")
238 def _get_dav_lockdiscovery(self, uri):
239 """ We raise that so that the node API is used """
242 def _get_dav_supportedlock(self, uri):
243 """ We raise that so that the node API is used """
246 def match_prop(self, uri, match, ns, propname):
247 if self.M_NS.has_key(ns):
248 return match == dav_interface.get_prop(self, uri, ns, propname)
249 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
253 node = self.uri2object(cr, uid, pool, uri2)
257 res = node.match_dav_eprop(cr, match, ns, propname)
261 def prep_http_options(self, uri, opts):
262 """see HttpOptions._prep_OPTIONS """
263 self.parent.log_message('get options: %s' % uri)
264 cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
269 node = self.uri2object(cr, uid, pool, uri2[:])
275 if hasattr(node, 'http_options'):
277 for key, val in node.http_options.items():
278 if isinstance(val, basestring):
281 ret[key] = ret[key][:] # copy the orig. array
286 self.parent.log_message('options: %s' % ret)
292 def reduce_useragent(self):
293 ua = self.parent.headers.get('User-Agent', False)
297 ctx['DAV-client'] = 'iPhone'
298 elif 'Konqueror' in ua:
299 ctx['DAV-client'] = 'GroupDAV'
302 def get_prop(self, uri, ns, propname):
303 """ return the value of a given property
305 uri -- uri of the object to get the property of
306 ns -- namespace of the property
307 pname -- name of the property
309 if self.M_NS.has_key(ns):
311 # if it's not in the interface class, a "DAV:" property
312 # may be at the node class. So shouldn't give up early.
313 return dav_interface.get_prop(self, uri, ns, propname)
316 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
321 node = self.uri2object(cr, uid, pool, uri2)
324 res = node.get_dav_eprop(cr, ns, propname)
329 def get_db(self, uri, rest_ret=False, allow_last=False):
330 """Parse the uri and get the dbname and the rest.
331 Db name should be the first component in the unix-like
332 path supplied in uri.
334 @param rest_ret Instead of the db_name, return (db_name, rest),
335 where rest is the remaining path
336 @param allow_last If the dbname is the last component in the
337 path, allow it to be resolved. The default False value means
338 we will not attempt to use the db, unless there is more
341 @return db_name or (dbname, rest) depending on rest_ret,
342 will return dbname=False when component is not found.
345 uri2 = self.uri2local(uri)
346 if uri2.startswith('/'):
348 names=uri2.split('/',1)
355 if len(names) > ll and names[0]:
366 def urijoin(self,*ajoin):
367 """ Return the base URI of this request, or even join it with the
370 return self.parent.get_baseuri(self) + '/'.join(ajoin)
373 def _all_db_list(self):
374 """return all databases who have module document_webdav installed"""
375 s = netsvc.ExportService.getService('db')
376 result = s.exp_list()
378 for db_name in result:
381 db = sql_db.db_connect(db_name)
383 cr.execute("SELECT id FROM ir_module_module WHERE name = 'document_webdav' AND state='installed' ")
386 self.db_name_list.append(db_name)
388 self.parent.log_error("Exception in db list: %s" % e)
392 return self.db_name_list
394 def db_list(self, uri):
395 # import pudb;pudb.set_trace()
396 u = urlparse.urlsplit(uri)
399 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d',d)
400 dbs = [i for i in self._all_db_list() if re.match(r, i)]
403 def get_childs(self,uri, filters=None):
404 """ return the child objects as self.baseuris for the given URI """
405 self.parent.log_message('get children: %s' % uri)
406 cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
410 res = map(lambda x: self.urijoin(x), self.db_list(uri))
413 node = self.uri2object(cr, uid, pool, uri2[:])
417 raise DAV_NotFound2(uri2)
419 fp = node.full_path()
422 self.parent.log_message('children for: %s' % fp)
427 domain = node.get_domain(cr, filters)
429 if hasattr(filters, 'getElementsByTagNameNS'):
430 hrefs = filters.getElementsByTagNameNS('DAV:', 'href')
432 ul = self.parent.davpath + self.uri2local(uri)
435 for tx in hr.childNodes:
436 if tx.nodeType == hr.TEXT_NODE:
438 if not turi.startswith('/'):
439 # it may be an absolute URL, decode to the
440 # relative part, because ul is relative, anyway
441 uparts=urlparse.urlparse(turi)
444 turi += ';' + uparts[3]
445 if turi.startswith(ul):
446 result.append( turi[len(self.parent.davpath):])
448 self.parent.log_error("ignore href %s because it is not under request path %s", turi, ul)
450 # We don't want to continue with the children found below
451 # Note the exceptions and that 'finally' will close the
453 for d in node.children(cr, domain):
454 self.parent.log_message('child: %s' % d.path)
456 result.append( self.urijoin(dbname,fp,d.path) )
458 result.append( self.urijoin(dbname,d.path) )
462 self.parent.log_error("Cannot get_children: "+str(e)+".")
468 def uri2local(self, uri):
469 uparts=urlparse.urlparse(uri)
472 reluri += ';'+uparts[3]
473 if reluri and reluri[-1]=="/":
478 # pos: -1 to get the parent of the uri
480 def get_cr(self, uri, allow_last=False):
481 """ Split the uri, grab a cursor for that db
483 pdb = self.parent.auth_provider.last_auth
484 dbname, uri2 = self.get_db(uri, rest_ret=True, allow_last=allow_last)
485 uri2 = (uri2 and uri2.split('/')) or []
487 return None, None, None, False, uri2
488 # if dbname was in our uri, we should have authenticated
490 assert pdb == dbname, " %s != %s" %(pdb, dbname)
491 res = self.parent.auth_provider.auth_creds.get(dbname, False)
493 self.parent.auth_provider.checkRequest(self.parent, uri, dbname)
494 res = self.parent.auth_provider.auth_creds[dbname]
495 user, passwd, dbn2, uid = res
496 db,pool = pooler.get_db_and_pool(dbname)
498 return cr, uid, pool, dbname, uri2
500 def uri2object(self, cr, uid, pool, uri):
503 context = self.reduce_useragent()
504 return pool.get('document.directory').get_object(cr, uid, uri, context=context)
506 def get_data(self,uri, rrange=None):
507 self.parent.log_message('GET: %s' % uri)
508 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
512 node = self.uri2object(cr, uid, pool, uri2)
514 raise DAV_NotFound2(uri2)
515 # TODO: if node is a collection, for some specific set of
516 # clients ( web browsers; available in node context),
517 # we may return a pseydo-html page with the directory listing.
519 res = node.open_data(cr,'r')
521 assert isinstance(rrange, (tuple,list))
522 start, end = map(long, rrange)
526 if end and end < start:
527 self.parent.log_error("Invalid range for data: %s-%s" %(start, end))
528 raise DAV_Error(416, "Invalid range for data.")
530 if end >= res.size():
531 raise DAV_Error(416, "Requested data exceeds available size.")
532 length = (end + 1) - start
534 length = res.size() - start
535 res = BoundStream2(res, offset=start, length=length)
538 # for the collections that return this error, the DAV standard
539 # says we'd better just return 200 OK with empty data
541 except IndexError,e :
542 self.parent.log_error("GET IndexError: %s", str(e))
543 raise DAV_NotFound2(uri2)
546 self.parent.log_error("GET exception: %s",str(e))
547 self.parent.log_message("Exc: %s", traceback.format_exc())
554 def _get_dav_resourcetype(self, uri):
555 """ return type of object """
556 self.parent.log_message('get RT: %s' % uri)
557 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
561 node = self.uri2object(cr, uid, pool, uri2)
563 raise DAV_NotFound2(uri2)
565 return node.get_dav_resourcetype(cr)
566 except NotImplementedError:
567 if node.type in ('collection','database'):
568 return ('collection', 'DAV:')
573 def _get_dav_displayname(self,uri):
574 self.parent.log_message('get DN: %s' % uri)
575 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
578 # at root, dbname, just return the last component
580 if uri2 and len(uri2) < 2:
583 node = self.uri2object(cr, uid, pool, uri2)
586 raise DAV_NotFound2(uri2)
588 return node.displayname
591 def _get_dav_getcontentlength(self, uri):
592 """ return the content length of an object """
593 self.parent.log_message('get length: %s' % uri)
595 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
599 node = self.uri2object(cr, uid, pool, uri2)
602 raise DAV_NotFound2(uri2)
603 result = node.content_length or 0
608 def _get_dav_getetag(self,uri):
609 """ return the ETag of an object """
610 self.parent.log_message('get etag: %s' % uri)
612 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
616 node = self.uri2object(cr, uid, pool, uri2)
619 raise DAV_NotFound2(uri2)
620 result = self._try_function(node.get_etag ,(cr,), "etag %s" %uri, cr=cr)
625 def get_lastmodified(self, uri):
626 """ return the last modified date of the object """
627 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
631 node = self.uri2object(cr, uid, pool, uri2)
633 raise DAV_NotFound2(uri2)
634 return _str2time(node.write_date)
638 def _get_dav_getlastmodified(self,uri):
639 """ return the last modified date of a resource
641 d=self.get_lastmodified(uri)
642 # format it. Note that we explicitly set the day, month names from
643 # an array, so that strftime() doesn't use its own locale-aware
646 return time.strftime("%%s, %d %%s %Y %H:%M:%S GMT", gmt ) % \
647 (day_names[gmt.tm_wday], month_names[gmt.tm_mon])
650 def get_creationdate(self, uri):
651 """ return the last modified date of the object """
652 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
656 node = self.uri2object(cr, uid, pool, uri2)
658 raise DAV_NotFound2(uri2)
660 return _str2time(node.create_date)
665 def _get_dav_getcontenttype(self,uri):
666 self.parent.log_message('get contenttype: %s' % uri)
667 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
670 return 'httpd/unix-directory'
672 node = self.uri2object(cr, uid, pool, uri2)
674 raise DAV_NotFound2(uri2)
675 result = str(node.mimetype)
677 #raise DAV_NotFound, 'Could not find %s' % path
682 """ create a new collection
683 see par. 9.3 of rfc4918
685 self.parent.log_message('MKCOL: %s' % uri)
686 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
689 raise DAV_Error(409, "Cannot create nameless collection.")
693 node = self.uri2object(cr,uid,pool, uri2[:-1])
696 raise DAV_Error(409, "Parent path %s does not exist" % uri2[:-1])
697 nc = node.child(cr, uri2[-1])
700 raise DAV_Error(405, "Path already exists.")
701 self._try_function(node.create_child_collection, (cr, uri2[-1]),
702 "create col %s" % uri2[-1], cr=cr)
707 def put(self, uri, data, content_type=None):
708 """ put the object into the filesystem """
709 self.parent.log_message('Putting %s (%d), %s'%( misc.ustr(uri), data and len(data) or 0, content_type))
710 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
715 node = self.uri2object(cr, uid, pool, uri2[:])
719 objname = misc.ustr(uri2[-1])
723 dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
726 raise DAV_NotFound('Parent folder not found.')
727 newchild = self._try_function(dir_node.create_child, (cr, objname, data),
728 "create %s" % objname, cr=cr)
732 raise DAV_Error(400, "Failed to create resource.")
734 uparts=urlparse.urlparse(uri)
735 fileloc = '/'.join(newchild.full_path())
736 if isinstance(fileloc, unicode):
737 fileloc = fileloc.encode('utf-8')
738 # the uri we get is a mangled one, where the davpath has been removed
739 davpath = self.parent.get_davpath()
741 surl = '%s://%s' % (uparts[0], uparts[1])
742 uloc = urllib.quote(fileloc)
744 if uri != ('/'+uloc) and uri != (surl + '/' + uloc):
745 hurl = '%s%s/%s/%s' %(surl, davpath, dbname, uloc)
748 etag = str(newchild.get_etag(cr))
750 self.parent.log_error("Cannot get etag for node: %s" % e)
751 ret = (str(hurl), etag)
753 self._try_function(node.set_data, (cr, data), "save %s" % objname, cr=cr)
760 """ delete a collection """
761 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
766 node = self.uri2object(cr, uid, pool, uri2)
767 self._try_function(node.rmcol, (cr,), "rmcol %s" % uri, cr=cr)
774 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
778 node = self.uri2object(cr, uid, pool, uri2)
779 res = self._try_function(node.rm, (cr,), "rm %s" % uri, cr=cr)
782 raise OSError(1, 'Invalid Action!')
787 ### DELETE handlers (examples)
788 ### (we use the predefined methods in davcmd instead of doing
792 def delone(self, uri):
793 """ delete a single resource
795 You have to return a result dict of the form
797 or None if everything's ok
800 if uri[-1]=='/':uri=uri[:-1]
802 # parent='/'.join(uri.split('/')[:-1])
805 def deltree(self, uri):
806 """ delete a collection
808 You have to return a result dict of the form
810 or None if everything's ok
812 if uri[-1]=='/':uri=uri[:-1]
813 res=deltree(self, uri)
814 # parent='/'.join(uri.split('/')[:-1])
819 ### MOVE handlers (examples)
822 def moveone(self, src, dst, overwrite):
823 """ move one resource with Depth=0
825 an alternative implementation would be
830 r=os.system("rm -f '%s'" %dst)
832 r=os.system("mv '%s' '%s'" %(src,dst))
836 (untested!). This would not use the davcmd functions
837 and thus can only detect errors directly on the root node.
839 res=moveone(self, src, dst, overwrite)
842 def movetree(self, src, dst, overwrite):
843 """ move a collection with Depth=infinity
845 an alternative implementation would be
850 r=os.system("rm -rf '%s'" %dst)
852 r=os.system("mv '%s' '%s'" %(src,dst))
856 (untested!). This would not use the davcmd functions
857 and thus can only detect errors directly on the root node"""
859 res=movetree(self, src, dst, overwrite)
866 def copyone(self, src, dst, overwrite):
867 """ copy one resource with Depth=0
869 an alternative implementation would be
874 r=os.system("rm -f '%s'" %dst)
876 r=os.system("cp '%s' '%s'" %(src,dst))
880 (untested!). This would not use the davcmd functions
881 and thus can only detect errors directly on the root node.
883 res=copyone(self, src, dst, overwrite)
886 def copytree(self, src, dst, overwrite):
887 """ copy a collection with Depth=infinity
889 an alternative implementation would be
894 r=os.system("rm -rf '%s'" %dst)
896 r=os.system("cp -r '%s' '%s'" %(src,dst))
900 (untested!). This would not use the davcmd functions
901 and thus can only detect errors directly on the root node"""
902 res=copytree(self, src, dst, overwrite)
907 ### This methods actually copy something. low-level
908 ### They are called by the davcmd utility functions
909 ### copytree and copyone (not the above!)
910 ### Look in davcmd.py for further details.
913 def copy(self, src, dst):
914 src=urllib.unquote(src)
915 dst=urllib.unquote(dst)
916 ct = self._get_dav_getcontenttype(src)
917 data = self.get_data(src)
918 self.put(dst, data, ct)
921 def copycol(self, src, dst):
922 """ copy a collection.
924 As this is not recursive (the davserver recurses itself)
925 we will only create a new directory here. For some more
926 advanced systems we might also have to copy properties from
927 the source to the destination.
929 return self.mkcol(dst)
932 def exists(self, uri):
933 """ test if a resource exists """
935 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
940 node = self.uri2object(cr, uid, pool, uri2)
948 def unlock(self, uri, token):
949 """ Unlock a resource from that token
951 @return True if unlocked, False if no lock existed, Exceptions
953 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
958 node = self.uri2object(cr, uid, pool, uri2)
960 node_fn = node.dav_unlock
961 except AttributeError:
962 # perhaps the node doesn't support locks
964 raise DAV_Error(400, 'No locks for this resource.')
966 res = self._try_function(node_fn, (cr, token), "unlock %s" % uri, cr=cr)
971 def lock(self, uri, lock_data):
972 """ Lock (may create) resource.
973 Data is a dict, may contain:
974 depth, token, refresh, lockscope, locktype, owner
976 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
983 node = self.uri2object(cr, uid, pool, uri2[:])
987 objname = misc.ustr(uri2[-1])
990 dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
993 raise DAV_NotFound('Parent folder not found.')
995 # We create a new node (file) but with empty data=None,
996 # as in RFC4918 p. 9.10.4
997 node = self._try_function(dir_node.create_child, (cr, objname, None),
998 "create %s" % objname, cr=cr)
1002 raise DAV_Error(400, "Failed to create resource.")
1007 node_fn = node.dav_lock
1008 except AttributeError:
1009 # perhaps the node doesn't support locks
1011 raise DAV_Error(400, 'No locks for this resource.')
1013 # Obtain the lock on the node
1014 lres, pid, token = self._try_function(node_fn, (cr, lock_data), "lock %s" % objname, cr=cr)
1019 raise DAV_Error(423, "Resource already locked.")
1021 assert isinstance(lres, list), 'lres: %s' % repr(lres)
1024 data = mk_lock_response(self, uri, lres)
1031 return created, data, token
1033 @memoize(CACHE_SIZE)
1034 def is_collection(self, uri):
1035 """ test if the given uri is a collection """
1036 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
1040 node = self.uri2object(cr,uid,pool, uri2)
1042 raise DAV_NotFound2(uri2)
1043 if node.type in ('collection','database'):
1051 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: