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 sql_db
41 import openerp.service
42 from openerp.tools import misc
44 from cache import memoize
45 from webdav import mk_lock_response
49 #hack for urlparse: add webdav in the net protocols
50 urlparse.uses_netloc.append('webdav')
51 urlparse.uses_netloc.append('webdavs')
53 day_names = { 0: 'Mon', 1: 'Tue' , 2: 'Wed', 3: 'Thu', 4: 'Fri', 5: 'Sat', 6: 'Sun' }
54 month_names = { 1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
55 7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec' }
57 def dict_merge2(*dicts):
58 """ Return a dict with all values of dicts.
59 If some key appears twice and contains iterable objects, the values
60 are merged (instead of overwritten).
65 if k in res and isinstance(res[k], (list, tuple)):
66 res[k] = res[k] + d[k]
67 elif k in res and isinstance(res[k], dict):
73 class DAV_NotFound2(DAV_NotFound):
74 """404 exception, that accepts our list uris
76 def __init__(self, *args):
77 if len(args) and isinstance(args[0], (tuple, list)):
78 path = ''.join([ '/' + x for x in args[0]])
80 DAV_NotFound.__init__(self, *args)
84 """ Convert a string with time representation (from db) into time (float)
89 if isinstance(cre, basestring) and '.' in cre:
91 frac = float(cre[fdot:])
93 return time.mktime(time.strptime(cre,'%Y-%m-%d %H:%M:%S')) + frac
95 class BoundStream2(object):
96 """Wraps around a seekable buffer, reads a determined range of data
98 Note that the supplied stream object MUST support a size() which
99 should return its data length (in bytes).
101 A variation of the class in websrv_lib.py
104 def __init__(self, stream, offset=None, length=None, chunk_size=None):
105 self._stream = stream
106 self._offset = offset or 0
107 self._length = length or self._stream.size()
108 self._rem_length = length
109 assert length and isinstance(length, (int, long))
110 assert length and length >= 0, length
111 self._chunk_size = chunk_size
112 if offset is not None:
113 self._stream.seek(offset)
115 def read(self, size=-1):
117 raise IOError(errno.EBADF, "read() without stream.")
119 if self._rem_length == 0:
121 elif self._rem_length < 0:
124 rsize = self._rem_length
125 if size > 0 and size < rsize:
127 if self._chunk_size and self._chunk_size < rsize:
128 rsize = self._chunk_size
130 data = self._stream.read(rsize)
131 self._rem_length -= len(data)
139 res = self._stream.tell()
148 return self.read(65536)
150 def seek(self, pos, whence=os.SEEK_SET):
151 """ Seek, computing our limited range
153 if whence == os.SEEK_SET:
154 if pos < 0 or pos > self._length:
155 raise IOError(errno.EINVAL,"Cannot seek.")
156 self._stream.seek(pos - self._offset)
157 self._rem_length = self._length - pos
158 elif whence == os.SEEK_CUR:
160 if pos > self._rem_length:
161 raise IOError(errno.EINVAL,"Cannot seek past end.")
165 raise IOError(errno.EINVAL,"Cannot seek before start.")
166 self._stream.seek(pos, os.SEEK_CUR)
167 self._rem_length -= pos
168 elif whence == os.SEEK_END:
170 raise IOError(errno.EINVAL,"Cannot seek past end.")
172 if self._length + pos < 0:
173 raise IOError(errno.EINVAL,"Cannot seek before start.")
174 newpos = self._offset + self._length + pos
175 self._stream.seek(newpos, os.SEEK_SET)
176 self._rem_length = 0 - pos
178 class openerp_dav_handler(dav_interface):
180 This class models a OpenERP interface for the DAV server
182 PROPS={'DAV:': dav_interface.PROPS['DAV:'],}
184 M_NS={ "DAV:" : dav_interface.M_NS['DAV:'],}
186 def __init__(self, parent, verbose=False):
189 self.baseuri = parent.baseuri
190 self.verbose = verbose
192 def get_propnames(self, uri):
194 self.parent.log_message('get propnames: %s' % uri)
195 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
198 # TODO: maybe limit props for databases..?
200 node = self.uri2object(cr, uid, pool, uri2)
202 props = dict_merge2(props, node.get_dav_props(cr))
206 def _try_function(self, funct, args, opname='run function', cr=None,
207 default_exc=DAV_Forbidden):
208 """ Try to run a function, and properly convert exceptions to DAV ones.
210 @objname the name of the operation being performed
211 @param cr if given, the cursor to close at exceptions
219 except NotImplementedError, e:
222 self.parent.log_error("Cannot %s: %s", opname, str(e))
223 self.parent.log_message("Exc: %s",traceback.format_exc())
224 # see par 9.3.1 of rfc
225 raise DAV_Error(403, str(e) or 'Not supported at this path.')
226 except EnvironmentError, err:
229 self.parent.log_error("Cannot %s: %s", opname, err.strerror)
230 self.parent.log_message("Exc: %s",traceback.format_exc())
231 raise default_exc(err.strerror)
235 self.parent.log_error("Cannot %s: %s", opname, str(e))
236 self.parent.log_message("Exc: %s",traceback.format_exc())
237 raise default_exc("Operation failed.")
239 def _get_dav_lockdiscovery(self, uri):
240 """ We raise that so that the node API is used """
243 def _get_dav_supportedlock(self, uri):
244 """ We raise that so that the node API is used """
247 def match_prop(self, uri, match, ns, propname):
248 if self.M_NS.has_key(ns):
249 return match == dav_interface.get_prop(self, uri, ns, propname)
250 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
254 node = self.uri2object(cr, uid, pool, uri2)
258 res = node.match_dav_eprop(cr, match, ns, propname)
262 def prep_http_options(self, uri, opts):
263 """see HttpOptions._prep_OPTIONS """
264 self.parent.log_message('get options: %s' % uri)
265 cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
270 node = self.uri2object(cr, uid, pool, uri2[:])
276 if hasattr(node, 'http_options'):
278 for key, val in node.http_options.items():
279 if isinstance(val, basestring):
282 ret[key] = ret[key][:] # copy the orig. array
287 self.parent.log_message('options: %s' % ret)
293 def reduce_useragent(self):
294 ua = self.parent.headers.get('User-Agent', False)
298 ctx['DAV-client'] = 'iPhone'
299 elif 'Konqueror' in ua:
300 ctx['DAV-client'] = 'GroupDAV'
303 def get_prop(self, uri, ns, propname):
304 """ return the value of a given property
306 uri -- uri of the object to get the property of
307 ns -- namespace of the property
308 pname -- name of the property
310 if self.M_NS.has_key(ns):
312 # if it's not in the interface class, a "DAV:" property
313 # may be at the node class. So shouldn't give up early.
314 return dav_interface.get_prop(self, uri, ns, propname)
317 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
322 node = self.uri2object(cr, uid, pool, uri2)
325 res = node.get_dav_eprop(cr, ns, propname)
330 def get_db(self, uri, rest_ret=False, allow_last=False):
331 """Parse the uri and get the dbname and the rest.
332 Db name should be the first component in the unix-like
333 path supplied in uri.
335 @param rest_ret Instead of the db_name, return (db_name, rest),
336 where rest is the remaining path
337 @param allow_last If the dbname is the last component in the
338 path, allow it to be resolved. The default False value means
339 we will not attempt to use the db, unless there is more
342 @return db_name or (dbname, rest) depending on rest_ret,
343 will return dbname=False when component is not found.
346 uri2 = self.uri2local(uri)
347 if uri2.startswith('/'):
349 names=uri2.split('/',1)
356 if len(names) > ll and names[0]:
367 def urijoin(self,*ajoin):
368 """ Return the base URI of this request, or even join it with the
371 return self.parent.get_baseuri(self) + '/'.join(ajoin)
374 def _all_db_list(self):
375 """return all databases who have module document_webdav installed"""
376 s = openerp.service.db
377 result = s.exp_list()
379 for db_name in result:
382 db = sql_db.db_connect(db_name)
384 cr.execute("SELECT id FROM ir_module_module WHERE name = 'document_webdav' AND state='installed' ")
387 self.db_name_list.append(db_name)
389 self.parent.log_error("Exception in db list: %s" % e)
393 return self.db_name_list
395 def db_list(self, uri):
396 # import pudb;pudb.set_trace()
397 u = urlparse.urlsplit(uri)
400 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d',d)
401 dbs = [i for i in self._all_db_list() if re.match(r, i)]
404 def get_childs(self,uri, filters=None):
405 """ return the child objects as self.baseuris for the given URI """
406 self.parent.log_message('get children: %s' % uri)
407 cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
411 res = map(lambda x: self.urijoin(x), self.db_list(uri))
414 node = self.uri2object(cr, uid, pool, uri2[:])
418 raise DAV_NotFound2(uri2)
420 fp = node.full_path()
423 self.parent.log_message('children for: %s' % fp)
428 domain = node.get_domain(cr, filters)
430 if hasattr(filters, 'getElementsByTagNameNS'):
431 hrefs = filters.getElementsByTagNameNS('DAV:', 'href')
433 ul = self.parent.davpath + self.uri2local(uri)
436 for tx in hr.childNodes:
437 if tx.nodeType == hr.TEXT_NODE:
439 if not turi.startswith('/'):
440 # it may be an absolute URL, decode to the
441 # relative part, because ul is relative, anyway
442 uparts=urlparse.urlparse(turi)
445 turi += ';' + uparts[3]
446 if turi.startswith(ul):
447 result.append( turi[len(self.parent.davpath):])
449 self.parent.log_error("ignore href %s because it is not under request path %s", turi, ul)
451 # We don't want to continue with the children found below
452 # Note the exceptions and that 'finally' will close the
454 for d in node.children(cr, domain):
455 self.parent.log_message('child: %s' % d.path)
457 result.append( self.urijoin(dbname,fp,d.path) )
459 result.append( self.urijoin(dbname,d.path) )
463 self.parent.log_error("Cannot get_children: "+str(e)+".")
469 def uri2local(self, uri):
470 uparts=urlparse.urlparse(uri)
473 reluri += ';'+uparts[3]
474 if reluri and reluri[-1]=="/":
479 # pos: -1 to get the parent of the uri
481 def get_cr(self, uri, allow_last=False):
482 """ Split the uri, grab a cursor for that db
484 pdb = self.parent.auth_provider.last_auth
485 dbname, uri2 = self.get_db(uri, rest_ret=True, allow_last=allow_last)
486 uri2 = (uri2 and uri2.split('/')) or []
488 return None, None, None, False, uri2
489 # if dbname was in our uri, we should have authenticated
491 assert pdb == dbname, " %s != %s" %(pdb, dbname)
492 res = self.parent.auth_provider.auth_creds.get(dbname, False)
494 self.parent.auth_provider.checkRequest(self.parent, uri, dbname)
495 res = self.parent.auth_provider.auth_creds[dbname]
496 user, passwd, dbn2, uid = res
497 registry = openerp.registry(dbname)
498 cr = registery.db.cursor()
499 return cr, uid, registry, dbname, uri2
501 def uri2object(self, cr, uid, pool, uri):
504 context = self.reduce_useragent()
505 return pool.get('document.directory').get_object(cr, uid, uri, context=context)
507 def get_data(self,uri, rrange=None):
508 self.parent.log_message('GET: %s' % uri)
509 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
513 node = self.uri2object(cr, uid, pool, uri2)
515 raise DAV_NotFound2(uri2)
516 # TODO: if node is a collection, for some specific set of
517 # clients ( web browsers; available in node context),
518 # we may return a pseydo-html page with the directory listing.
520 res = node.open_data(cr,'r')
522 assert isinstance(rrange, (tuple,list))
523 start, end = map(long, rrange)
527 if end and end < start:
528 self.parent.log_error("Invalid range for data: %s-%s" %(start, end))
529 raise DAV_Error(416, "Invalid range for data.")
531 if end >= res.size():
532 raise DAV_Error(416, "Requested data exceeds available size.")
533 length = (end + 1) - start
535 length = res.size() - start
536 res = BoundStream2(res, offset=start, length=length)
539 # for the collections that return this error, the DAV standard
540 # says we'd better just return 200 OK with empty data
542 except IndexError,e :
543 self.parent.log_error("GET IndexError: %s", str(e))
544 raise DAV_NotFound2(uri2)
547 self.parent.log_error("GET exception: %s",str(e))
548 self.parent.log_message("Exc: %s", traceback.format_exc())
555 def _get_dav_resourcetype(self, uri):
556 """ return type of object """
557 self.parent.log_message('get RT: %s' % uri)
558 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
562 node = self.uri2object(cr, uid, pool, uri2)
564 raise DAV_NotFound2(uri2)
566 return node.get_dav_resourcetype(cr)
567 except NotImplementedError:
568 if node.type in ('collection','database'):
569 return ('collection', 'DAV:')
574 def _get_dav_displayname(self,uri):
575 self.parent.log_message('get DN: %s' % uri)
576 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
579 # at root, dbname, just return the last component
581 if uri2 and len(uri2) < 2:
584 node = self.uri2object(cr, uid, pool, uri2)
587 raise DAV_NotFound2(uri2)
589 return node.displayname
592 def _get_dav_getcontentlength(self, uri):
593 """ return the content length of an object """
594 self.parent.log_message('get length: %s' % uri)
596 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
600 node = self.uri2object(cr, uid, pool, uri2)
603 raise DAV_NotFound2(uri2)
604 result = node.content_length or 0
609 def _get_dav_getetag(self,uri):
610 """ return the ETag of an object """
611 self.parent.log_message('get etag: %s' % uri)
613 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
617 node = self.uri2object(cr, uid, pool, uri2)
620 raise DAV_NotFound2(uri2)
621 result = self._try_function(node.get_etag ,(cr,), "etag %s" %uri, cr=cr)
626 def get_lastmodified(self, uri):
627 """ return the last modified date of the object """
628 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
632 node = self.uri2object(cr, uid, pool, uri2)
634 raise DAV_NotFound2(uri2)
635 return _str2time(node.write_date)
639 def _get_dav_getlastmodified(self,uri):
640 """ return the last modified date of a resource
642 d=self.get_lastmodified(uri)
643 # format it. Note that we explicitly set the day, month names from
644 # an array, so that strftime() doesn't use its own locale-aware
647 return time.strftime("%%s, %d %%s %Y %H:%M:%S GMT", gmt ) % \
648 (day_names[gmt.tm_wday], month_names[gmt.tm_mon])
651 def get_creationdate(self, uri):
652 """ return the last modified date of the object """
653 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
657 node = self.uri2object(cr, uid, pool, uri2)
659 raise DAV_NotFound2(uri2)
661 return _str2time(node.create_date)
666 def _get_dav_getcontenttype(self,uri):
667 self.parent.log_message('get contenttype: %s' % uri)
668 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
671 return 'httpd/unix-directory'
673 node = self.uri2object(cr, uid, pool, uri2)
675 raise DAV_NotFound2(uri2)
676 result = str(node.mimetype)
678 #raise DAV_NotFound, 'Could not find %s' % path
683 """ create a new collection
684 see par. 9.3 of rfc4918
686 self.parent.log_message('MKCOL: %s' % uri)
687 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
690 raise DAV_Error(409, "Cannot create nameless collection.")
694 node = self.uri2object(cr,uid,pool, uri2[:-1])
697 raise DAV_Error(409, "Parent path %s does not exist" % uri2[:-1])
698 nc = node.child(cr, uri2[-1])
701 raise DAV_Error(405, "Path already exists.")
702 self._try_function(node.create_child_collection, (cr, uri2[-1]),
703 "create col %s" % uri2[-1], cr=cr)
708 def put(self, uri, data, content_type=None):
709 """ put the object into the filesystem """
710 self.parent.log_message('Putting %s (%d), %s'%( misc.ustr(uri), data and len(data) or 0, content_type))
711 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
716 node = self.uri2object(cr, uid, pool, uri2[:])
720 objname = misc.ustr(uri2[-1])
724 dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
727 raise DAV_NotFound('Parent folder not found.')
728 newchild = self._try_function(dir_node.create_child, (cr, objname, data),
729 "create %s" % objname, cr=cr)
733 raise DAV_Error(400, "Failed to create resource.")
735 uparts=urlparse.urlparse(uri)
736 fileloc = '/'.join(newchild.full_path())
737 if isinstance(fileloc, unicode):
738 fileloc = fileloc.encode('utf-8')
739 # the uri we get is a mangled one, where the davpath has been removed
740 davpath = self.parent.get_davpath()
742 surl = '%s://%s' % (uparts[0], uparts[1])
743 uloc = urllib.quote(fileloc)
745 if uri != ('/'+uloc) and uri != (surl + '/' + uloc):
746 hurl = '%s%s/%s/%s' %(surl, davpath, dbname, uloc)
749 etag = str(newchild.get_etag(cr))
751 self.parent.log_error("Cannot get etag for node: %s" % e)
752 ret = (str(hurl), etag)
754 self._try_function(node.set_data, (cr, data), "save %s" % objname, cr=cr)
761 """ delete a collection """
762 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
767 node = self.uri2object(cr, uid, pool, uri2)
768 self._try_function(node.rmcol, (cr,), "rmcol %s" % uri, cr=cr)
775 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
779 node = self.uri2object(cr, uid, pool, uri2)
780 res = self._try_function(node.rm, (cr,), "rm %s" % uri, cr=cr)
783 raise OSError(1, 'Invalid Action!')
788 ### DELETE handlers (examples)
789 ### (we use the predefined methods in davcmd instead of doing
793 def delone(self, uri):
794 """ delete a single resource
796 You have to return a result dict of the form
798 or None if everything's ok
801 if uri[-1]=='/':uri=uri[:-1]
803 # parent='/'.join(uri.split('/')[:-1])
806 def deltree(self, uri):
807 """ delete a collection
809 You have to return a result dict of the form
811 or None if everything's ok
813 if uri[-1]=='/':uri=uri[:-1]
814 res=deltree(self, uri)
815 # parent='/'.join(uri.split('/')[:-1])
820 ### MOVE handlers (examples)
823 def moveone(self, src, dst, overwrite):
824 """ move one resource with Depth=0
826 an alternative implementation would be
831 r=os.system("rm -f '%s'" %dst)
833 r=os.system("mv '%s' '%s'" %(src,dst))
837 (untested!). This would not use the davcmd functions
838 and thus can only detect errors directly on the root node.
840 res=moveone(self, src, dst, overwrite)
843 def movetree(self, src, dst, overwrite):
844 """ move a collection with Depth=infinity
846 an alternative implementation would be
851 r=os.system("rm -rf '%s'" %dst)
853 r=os.system("mv '%s' '%s'" %(src,dst))
857 (untested!). This would not use the davcmd functions
858 and thus can only detect errors directly on the root node"""
860 res=movetree(self, src, dst, overwrite)
867 def copyone(self, src, dst, overwrite):
868 """ copy one resource with Depth=0
870 an alternative implementation would be
875 r=os.system("rm -f '%s'" %dst)
877 r=os.system("cp '%s' '%s'" %(src,dst))
881 (untested!). This would not use the davcmd functions
882 and thus can only detect errors directly on the root node.
884 res=copyone(self, src, dst, overwrite)
887 def copytree(self, src, dst, overwrite):
888 """ copy a collection with Depth=infinity
890 an alternative implementation would be
895 r=os.system("rm -rf '%s'" %dst)
897 r=os.system("cp -r '%s' '%s'" %(src,dst))
901 (untested!). This would not use the davcmd functions
902 and thus can only detect errors directly on the root node"""
903 res=copytree(self, src, dst, overwrite)
908 ### This methods actually copy something. low-level
909 ### They are called by the davcmd utility functions
910 ### copytree and copyone (not the above!)
911 ### Look in davcmd.py for further details.
914 def copy(self, src, dst):
915 src=urllib.unquote(src)
916 dst=urllib.unquote(dst)
917 ct = self._get_dav_getcontenttype(src)
918 data = self.get_data(src)
919 self.put(dst, data, ct)
922 def copycol(self, src, dst):
923 """ copy a collection.
925 As this is not recursive (the davserver recurses itself)
926 we will only create a new directory here. For some more
927 advanced systems we might also have to copy properties from
928 the source to the destination.
930 return self.mkcol(dst)
933 def exists(self, uri):
934 """ test if a resource exists """
936 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
941 node = self.uri2object(cr, uid, pool, uri2)
949 def unlock(self, uri, token):
950 """ Unlock a resource from that token
952 @return True if unlocked, False if no lock existed, Exceptions
954 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
959 node = self.uri2object(cr, uid, pool, uri2)
961 node_fn = node.dav_unlock
962 except AttributeError:
963 # perhaps the node doesn't support locks
965 raise DAV_Error(400, 'No locks for this resource.')
967 res = self._try_function(node_fn, (cr, token), "unlock %s" % uri, cr=cr)
972 def lock(self, uri, lock_data):
973 """ Lock (may create) resource.
974 Data is a dict, may contain:
975 depth, token, refresh, lockscope, locktype, owner
977 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
984 node = self.uri2object(cr, uid, pool, uri2[:])
988 objname = misc.ustr(uri2[-1])
991 dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
994 raise DAV_NotFound('Parent folder not found.')
996 # We create a new node (file) but with empty data=None,
997 # as in RFC4918 p. 9.10.4
998 node = self._try_function(dir_node.create_child, (cr, objname, None),
999 "create %s" % objname, cr=cr)
1003 raise DAV_Error(400, "Failed to create resource.")
1008 node_fn = node.dav_lock
1009 except AttributeError:
1010 # perhaps the node doesn't support locks
1012 raise DAV_Error(400, 'No locks for this resource.')
1014 # Obtain the lock on the node
1015 lres, pid, token = self._try_function(node_fn, (cr, lock_data), "lock %s" % objname, cr=cr)
1020 raise DAV_Error(423, "Resource already locked.")
1022 assert isinstance(lres, list), 'lres: %s' % repr(lres)
1025 data = mk_lock_response(self, uri, lres)
1032 return created, data, token
1034 @memoize(CACHE_SIZE)
1035 def is_collection(self, uri):
1036 """ test if the given uri is a collection """
1037 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
1041 node = self.uri2object(cr,uid,pool, uri2)
1043 raise DAV_NotFound2(uri2)
1044 if node.type in ('collection','database'):
1052 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: