[MERGE] lp:~xrg/openobject-addons/trunk-patch18
[odoo/odoo.git] / addons / document_webdav / dav_fs.py
index 1acbfd7..22f50cb 100644 (file)
-# -*- encoding: utf-8 -*-
+# -*- coding: utf-8 -*-
 ##############################################################################
 #
-# Copyright (c) 2004 TINY SPRL. (http://tiny.be) All Rights Reserved.
-#                    Fabien Pinckaers <fp@tiny.Be>
+#    OpenERP, Open Source Management Solution
+#    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
 #
-# WARNING: This program as such is intended to be used by professional
-# programmers who take the whole responsability of assessing all potential
-# consequences resulting from its eventual inadequacies and bugs
-# End users who are looking for a ready-to-use solution with commercial
-# garantees and support are strongly adviced to contract a Free Software
-# Service Company
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
 #
-# 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 Affero General Public License for more details.
 #
-# 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.
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
-
 import pooler
 
-import base64
-import sys
 import os
 import time
-from string import joinfields, split, lower
-
-from service import security
+import errno
 
 import netsvc
 import urlparse
 
-from DAV.constants import COLLECTION, OBJECT
-from DAV.errors import *
-from DAV.iface import *
+from DAV.constants import COLLECTION  #, OBJECT
+from DAV.errors import DAV_Error, DAV_Forbidden, DAV_NotFound
+from DAV.iface import dav_interface
 import urllib
 
 from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree
-from document.nodes import node_res_dir, node_res_obj
 from cache import memoize
 from tools import misc
+
+from webdav import mk_lock_response
+
+try:
+    from tools.dict_tools import dict_merge2
+except ImportError:
+    from document.dict_tools import dict_merge2
+
 CACHE_SIZE=20000
 
 #hack for urlparse: add webdav in the net protocols
 urlparse.uses_netloc.append('webdav')
 urlparse.uses_netloc.append('webdavs')
 
-class tinydav_handler(dav_interface):
+day_names = { 0: 'Mon', 1: 'Tue' , 2: 'Wed', 3: 'Thu', 4: 'Fri', 5: 'Sat', 6: 'Sun' }
+month_names = { 1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
+        7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec' }
+
+class DAV_NotFound2(DAV_NotFound):
+    """404 exception, that accepts our list uris
     """
-    This class models a Tiny ERP interface for the DAV server
+    def __init__(self, *args):
+        if len(args) and isinstance(args[0], (tuple, list)):
+            path = ''.join([ '/' + x for x in args[0]])
+            args = (path, )
+        DAV_NotFound.__init__(self, *args)
+
+
+def _str2time(cre):
+    """ Convert a string with time representation (from db) into time (float)
+    """
+    if not cre:
+        return time.time()
+    frac = 0.0
+    if isinstance(cre, basestring) and '.' in cre:
+        fdot = cre.find('.')
+        frac = float(cre[fdot:])
+        cre = cre[:fdot]
+    return time.mktime(time.strptime(cre,'%Y-%m-%d %H:%M:%S')) + frac
+
+class BoundStream2(object):
+    """Wraps around a seekable buffer, reads a determined range of data
+    
+        Note that the supplied stream object MUST support a size() which
+        should return its data length (in bytes).
+    
+        A variation of the class in websrv_lib.py
     """
-    PROPS={'DAV:': dav_interface.PROPS['DAV:'], }
+    
+    def __init__(self, stream, offset=None, length=None, chunk_size=None):
+        self._stream = stream
+        self._offset = offset or 0
+        self._length = length or self._stream.size()
+        self._rem_length = length
+        assert length and isinstance(length, (int, long))
+        assert length and length >= 0, length
+        self._chunk_size = chunk_size
+        if offset is not None:
+            self._stream.seek(offset)
+
+    def read(self, size=-1):
+        if not self._stream:
+            raise IOError(errno.EBADF, "read() without stream")
+        
+        if self._rem_length == 0:
+            return ''
+        elif self._rem_length < 0:
+            raise EOFError()
+
+        rsize = self._rem_length
+        if size > 0 and size < rsize:
+            rsize = size
+        if self._chunk_size and self._chunk_size < rsize:
+            rsize = self._chunk_size
+        
+        data = self._stream.read(rsize)
+        self._rem_length -= len(data)
 
-    M_NS={ "DAV:" : dav_interface.M_NS['DAV:'], }
+        return data
+
+    def __len__(self):
+        return self._length
+
+    def tell(self):
+        res = self._stream.tell()
+        if self._offset:
+            res -= self._offset
+        return res
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        return self.read(65536)
+
+    def seek(self, pos, whence=os.SEEK_SET):
+        """ Seek, computing our limited range
+        """
+        if whence == os.SEEK_SET:
+            if pos < 0 or pos > self._length:
+                raise IOError(errno.EINVAL,"Cannot seek")
+            self._stream.seek(pos - self._offset)
+            self._rem_length = self._length - pos
+        elif whence == os.SEEK_CUR:
+            if pos > 0:
+                if pos > self._rem_length:
+                    raise IOError(errno.EINVAL,"Cannot seek past end")
+                elif pos < 0:
+                    oldpos = self.tell()
+                    if oldpos + pos < 0:
+                        raise IOError(errno.EINVAL,"Cannot seek before start")
+                self._stream.seek(pos, os.SEEK_CUR)
+                self._rem_length -= pos
+        elif whence == os.SEEK_END:
+            if pos > 0:
+                raise IOError(errno.EINVAL,"Cannot seek past end")
+            else:
+                if self._length + pos < 0:
+                    raise IOError(errno.EINVAL,"Cannot seek before start")
+            newpos = self._offset + self._length + pos
+            self._stream.seek(newpos, os.SEEK_SET)
+            self._rem_length = 0 - pos
+
+class openerp_dav_handler(dav_interface):
+    """
+    This class models a OpenERP interface for the DAV server
+    """
+    PROPS={'DAV:': dav_interface.PROPS['DAV:'],}
+
+    M_NS={ "DAV:" : dav_interface.M_NS['DAV:'],}
 
     def __init__(self,  parent, verbose=False):
-        self.db_name = False
-        self.directory_id=False
         self.db_name_list=[]
         self.parent = parent
         self.baseuri = parent.baseuri
+        self.verbose = verbose
 
-        def get_propnames(self,uri):
-            props = self.PROPS
-            self.parent.log_message('get propnames: %s' % uri)
-            if uri[-1]=='/':uri=uri[:-1]
-            cr, uid, pool, dbname, uri2 = self.get_cr(uri)
-            if not dbname:
-                cr.close()
-                return props
-            node = self.uri2object(cr,uid,pool, uri2)
-            if node:
-                props.update(node.get_dav_props(cr))
-            cr.close()
+    def get_propnames(self, uri):
+        props = self.PROPS
+        self.parent.log_message('get propnames: %s' % uri)
+        cr, uid, pool, dbname, uri2 = self.get_cr(uri)
+        if not dbname:
+            if cr: cr.close()
+            # TODO: maybe limit props for databases..?
             return props
+        node = self.uri2object(cr, uid, pool, uri2)
+        if node:
+            props = dict_merge2(props, node.get_dav_props(cr))
+        cr.close()
+        return props
 
-        def get_prop(self,uri,ns,propname):
-            """ return the value of a given property
+    def _try_function(self, funct, args, opname='run function', cr=None,
+            default_exc=DAV_Forbidden):
+        """ Try to run a function, and properly convert exceptions to DAV ones.
 
-                uri        -- uri of the object to get the property of
-                ns        -- namespace of the property
-                pname        -- name of the property
-             """
-            if self.M_NS.has_key(ns):
-               return dav_interface.get_prop(self,uri,ns,propname)
+            @objname the name of the operation being performed
+            @param cr if given, the cursor to close at exceptions
+        """
 
-            if uri[-1]=='/':uri=uri[:-1]
-            cr, uid, pool, dbname, uri2 = self.get_cr(uri)
-            if not dbname:
-                cr.close()
-                raise DAV_NotFound
-            node = self.uri2object(cr,uid,pool, uri2)
+        try:
+            return funct(*args)
+        except DAV_Error:
+            if cr: cr.close()
+            raise
+        except NotImplementedError, e:
+            if cr: cr.close()
+            import traceback
+            self.parent.log_error("Cannot %s: %s", opname, str(e))
+            self.parent.log_message("Exc: %s",traceback.format_exc())
+            # see par 9.3.1 of rfc
+            raise DAV_Error(403, str(e) or 'Not supported at this path')
+        except EnvironmentError, err:
+            if cr: cr.close()
+            import traceback
+            self.parent.log_error("Cannot %s: %s", opname, err.strerror)
+            self.parent.log_message("Exc: %s",traceback.format_exc())
+            raise default_exc(err.strerror)
+        except Exception, e:
+            import traceback
+            if cr: cr.close()
+            self.parent.log_error("Cannot %s: %s", opname, str(e))
+            self.parent.log_message("Exc: %s",traceback.format_exc())
+            raise default_exc("Operation failed")
+
+    def _get_dav_lockdiscovery(self, uri):
+        """ We raise that so that the node API is used """
+        raise DAV_NotFound
+
+    def _get_dav_supportedlock(self, uri):
+        """ We raise that so that the node API is used """
+        raise DAV_NotFound
+
+    def match_prop(self, uri, match, ns, propname):
+        if self.M_NS.has_key(ns):
+            return match == dav_interface.get_prop(self, uri, ns, propname)
+        cr, uid, pool, dbname, uri2 = self.get_cr(uri)
+        if not dbname:
+            if cr: cr.close()
+            raise DAV_NotFound
+        node = self.uri2object(cr, uid, pool, uri2)
+        if not node:
+            cr.close()
+            raise DAV_NotFound
+        res = node.match_dav_eprop(cr, match, ns, propname)
+        cr.close()
+        return res
+
+    def prep_http_options(self, uri, opts):
+        """see HttpOptions._prep_OPTIONS """
+        self.parent.log_message('get options: %s' % uri)
+        cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
+
+        if not dbname:
+            if cr: cr.close()
+            return opts
+        node = self.uri2object(cr, uid, pool, uri2[:])
+
+        if not node:
+            if cr: cr.close()
+            return opts
+        else:
+            if hasattr(node, 'http_options'):
+                ret = opts.copy()
+                for key, val in node.http_options.items():
+                    if isinstance(val, basestring):
+                        val = [val, ]
+                    if key in ret:
+                        ret[key] = ret[key][:]  # copy the orig. array
+                    else:
+                        ret[key] = []
+                    ret[key].extend(val)
+
+                self.parent.log_message('options: %s' % ret)
+            else:
+                ret = opts
+            cr.close()
+            return ret
+
+    def reduce_useragent(self):
+        ua = self.parent.headers.get('User-Agent', False)
+        ctx = {}
+        if ua:
+            if 'iPhone' in ua:
+                ctx['DAV-client'] = 'iPhone'
+            elif 'Konqueror' in ua:
+                ctx['DAV-client'] = 'GroupDAV'
+        return ctx
+
+    def get_prop(self, uri, ns, propname):
+        """ return the value of a given property
+
+            uri        -- uri of the object to get the property of
+            ns        -- namespace of the property
+            pname        -- name of the property
+         """
+        if self.M_NS.has_key(ns):
+            try:
+                # if it's not in the interface class, a "DAV:" property
+                # may be at the node class. So shouldn't give up early.
+                return dav_interface.get_prop(self, uri, ns, propname)
+            except DAV_NotFound:
+                pass
+        cr, uid, pool, dbname, uri2 = self.get_cr(uri)
+        if not dbname:
+            if cr: cr.close()
+            raise DAV_NotFound
+        try:
+            node = self.uri2object(cr, uid, pool, uri2)
             if not node:
-                cr.close()
                 raise DAV_NotFound
-            res = node.get_dav_eprop(cr,ns,propname)
+            res = node.get_dav_eprop(cr, ns, propname)
+        finally:
             cr.close()
-            return res
+        return res
 
-#
-#    def get_db(self,uri):
-#        names=self.uri2local(uri).split('/')
-#        self.db_name=False
-#        if len(names) > 1:
-#            self.db_name=self.uri2local(uri).split('/')[1]
-#            if self.db_name=='':
-#                raise Exception,'Plese specify Database name in folder'
-#        return self.db_name
-#
+    def get_db(self, uri, rest_ret=False, allow_last=False):
+        """Parse the uri and get the dbname and the rest.
+           Db name should be the first component in the unix-like
+           path supplied in uri.
+
+           @param rest_ret Instead of the db_name, return (db_name, rest),
+                where rest is the remaining path
+           @param allow_last If the dbname is the last component in the
+                path, allow it to be resolved. The default False value means
+                we will not attempt to use the db, unless there is more
+                path.
+
+           @return db_name or (dbname, rest) depending on rest_ret,
+                will return dbname=False when component is not found.
+        """
+
+        uri2 = self.uri2local(uri)
+        if uri2.startswith('/'):
+            uri2 = uri2[1:]
+        names=uri2.split('/',1)
+        db_name=False
+        rest = None
+        if allow_last:
+            ll = 0
+        else:
+            ll = 1
+        if len(names) > ll and names[0]:
+            db_name = names[0]
+            names = names[1:]
+
+        if rest_ret:
+            if len(names):
+                rest = names[0]
+            return db_name, rest
+        return db_name
 
-    def later_get_db_from_path(self,path):
-        return "aaa"
 
     def urijoin(self,*ajoin):
         """ Return the base URI of this request, or even join it with the
             ajoin path elements
         """
-        return self.baseuri+ '/'.join(ajoin)
+        return self.parent.get_baseuri(self) + '/'.join(ajoin)
 
     @memoize(4)
     def db_list(self):
         s = netsvc.ExportService.getService('db')
-        result = s.list()
+        result = s.exp_list()
         self.db_name_list=[]
         for db_name in result:
-            db = pooler.get_db_only(db_name)
-            cr = db.cursor()
-            cr.execute("select id from ir_module_module where name = 'document' and state='installed' ")
-            res=cr.fetchone()
-            if res and len(res):
-                self.db_name_list.append(db_name)
-            cr.close()
+            cr = None
+            try:
+                db = pooler.get_db_only(db_name)
+                cr = db.cursor()
+                cr.execute("SELECT id FROM ir_module_module WHERE name = 'document' AND state='installed' ")
+                res=cr.fetchone()
+                if res and len(res):
+                    self.db_name_list.append(db_name)
+            except Exception, e:
+                self.parent.log_error("Exception in db list: %s" % e)
+            finally:
+                if cr:
+                    cr.close()
         return self.db_name_list
 
-    def get_childs(self,uri):
+    def get_childs(self,uri, filters=None):
         """ return the child objects as self.baseuris for the given URI """
-        self.parent.log_message('get childs: %s' % uri)
-        if uri[-1]=='/':uri=uri[:-1]
-        cr, uid, pool, dbname, uri2 = self.get_cr(uri)
-        
+        self.parent.log_message('get children: %s' % uri)
+        cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
+
         if not dbname:
-            s = netsvc.ExportService.getService('db')
-            cr.close()
-            return map(lambda x: self.urijoin(x), self.db_list())
+            if cr: cr.close()
+            res = map(lambda x: self.urijoin(x), self.db_list())
+            return res
         result = []
-        node = self.uri2object(cr,uid,pool, uri2[:])
-        if not node:
-            cr.close()
-            raise DAV_NotFound(uri2)
-        else:
-            fp = node.full_path()
-            if fp and len(fp):
-                self.parent.log_message('childs: @%s' % fp)
-                fp = '/'.join(fp)
+        node = self.uri2object(cr, uid, pool, uri2[:])
+
+        try:
+            if not node:
+                raise DAV_NotFound2(uri2)
             else:
-                fp = None
-            for d in node.children(cr):
-                self.parent.log_message('child: %s' % d.path)
-                if fp:
-                    result.append( self.urijoin(dbname,fp,d.path) )
+                fp = node.full_path()
+                if fp and len(fp):
+                    fp = '/'.join(fp)
+                    self.parent.log_message('children for: %s' % fp)
                 else:
-                    result.append( self.urijoin(dbname,d.path) )
-        cr.close()
+                    fp = None
+                domain = None
+                if filters:
+                    domain = node.get_domain(cr, filters)
+                    
+                    if hasattr(filters, 'getElementsByTagNameNS'):
+                        hrefs = filters.getElementsByTagNameNS('DAV:', 'href')
+                        if hrefs:
+                            ul = self.parent.davpath + self.uri2local(uri)
+                            for hr in hrefs:
+                                turi = ''
+                                for tx in hr.childNodes:
+                                    if tx.nodeType == hr.TEXT_NODE:
+                                        turi += tx.data
+                                if not turi.startswith('/'):
+                                    # it may be an absolute URL, decode to the
+                                    # relative part, because ul is relative, anyway
+                                    uparts=urlparse.urlparse(turi)
+                                    turi=uparts[2]
+                                if turi.startswith(ul):
+                                    result.append( turi[len(self.parent.davpath):])
+                                else:
+                                    self.parent.log_error("ignore href %s because it is not under request path %s", turi, ul)
+                            return result
+                            # We don't want to continue with the children found below
+                            # Note the exceptions and that 'finally' will close the
+                            # cursor
+                for d in node.children(cr, domain):
+                    self.parent.log_message('child: %s' % d.path)
+                    if fp:
+                        result.append( self.urijoin(dbname,fp,d.path) )
+                    else:
+                        result.append( self.urijoin(dbname,d.path) )
+        except DAV_Error:
+            raise
+        except Exception, e:
+            self.parent.log_error("cannot get_children: "+ str(e))
+            raise
+        finally:
+            if cr: cr.close()
         return result
 
     def uri2local(self, uri):
@@ -183,106 +447,129 @@ class tinydav_handler(dav_interface):
     #
     # pos: -1 to get the parent of the uri
     #
-    def get_cr(self, uri):        
+    def get_cr(self, uri, allow_last=False):
+        """ Split the uri, grab a cursor for that db
+        """
         pdb = self.parent.auth_proxy.last_auth
-        reluri = self.uri2local(uri)        
-        try:
-            dbname = reluri.split('/')[2]
-        except:
-            dbname = False
+        dbname, uri2 = self.get_db(uri, rest_ret=True, allow_last=allow_last)
+        uri2 = (uri2 and uri2.split('/')) or []
         if not dbname:
-            return None, None, None, False, None
-        if not pdb and dbname:
-            # if dbname was in our uri, we should have authenticated
-            # against that.
-            raise Exception("Programming error")        
+            return None, None, None, False, uri2
+        # if dbname was in our uri, we should have authenticated
+        # against that.
         assert pdb == dbname, " %s != %s" %(pdb, dbname)
-        user, passwd, dbn2, uid = self.parent.auth_proxy.auth_creds[pdb]
+        res = self.parent.auth_proxy.auth_creds.get(dbname, False)
+        if not res:
+            self.parent.auth_proxy.checkRequest(self.parent, uri, dbname)
+            res = self.parent.auth_proxy.auth_creds[dbname]
+        user, passwd, dbn2, uid = res
         db,pool = pooler.get_db_and_pool(dbname)
         cr = db.cursor()
-        uri2 = reluri.split('/')[3:]
         return cr, uid, pool, dbname, uri2
 
-    def uri2object(self, cr,uid, pool,uri):
+    def uri2object(self, cr, uid, pool, uri):
         if not uid:
             return None
-        return pool.get('document.directory').get_object(cr, uid, uri)
+        context = self.reduce_useragent()
+        return pool.get('document.directory').get_object(cr, uid, uri, context=context)
 
-    def get_data(self,uri):
+    def get_data(self,uri, rrange=None):
         self.parent.log_message('GET: %s' % uri)
-        if uri[-1]=='/':uri=uri[:-1]
         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
         try:
             if not dbname:
                 raise DAV_Error, 409
-            node = self.uri2object(cr,uid,pool, uri2)
+            node = self.uri2object(cr, uid, pool, uri2)
             if not node:
-                raise DAV_NotFound(uri2)
+                raise DAV_NotFound2(uri2)
+            # TODO: if node is a collection, for some specific set of
+            # clients ( web browsers; available in node context), 
+            # we may return a pseydo-html page with the directory listing.
             try:
-                datas = node.get_data(cr)
+                res = node.open_data(cr,'r')
+                if rrange:
+                    assert isinstance(rrange, (tuple,list))
+                    start, end = map(long, rrange)
+                    if not start:
+                        start = 0
+                    assert start >= 0
+                    if end and end < start:
+                        self.parent.log_error("Invalid range for data: %s-%s" %(start, end))
+                        raise DAV_Error(416, "Invalid range for data")
+                    if end:
+                        if end >= res.size():
+                            raise DAV_Error(416, "Requested data exceeds available size")
+                        length = (end + 1) - start
+                    else:
+                        length = res.size() - start
+                    res = BoundStream2(res, offset=start, length=length)
+                
             except TypeError,e:
-                import traceback
-                self.parent.log_error("GET typeError: %s", str(e))
-                self.parent.log_message("Exc: %s",traceback.format_exc())
-                raise DAV_Forbidden
+                # for the collections that return this error, the DAV standard
+                # says we'd better just return 200 OK with empty data
+                return ''
             except IndexError,e :
                 self.parent.log_error("GET IndexError: %s", str(e))
-                raise DAV_NotFound(uri2)
+                raise DAV_NotFound2(uri2)
             except Exception,e:
                 import traceback
                 self.parent.log_error("GET exception: %s",str(e))
                 self.parent.log_message("Exc: %s", traceback.format_exc())
                 raise DAV_Error, 409
-            return datas
+            return res
         finally:
-            cr.close()
+            if cr: cr.close()
 
     @memoize(CACHE_SIZE)
-    def _get_dav_resourcetype(self,uri):
+    def _get_dav_resourcetype(self, uri):
         """ return type of object """
         self.parent.log_message('get RT: %s' % uri)
-        if uri[-1]=='/':uri=uri[:-1]
         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
         try:
             if not dbname:
                 return COLLECTION
-            node = self.uri2object(cr,uid,pool, uri2)
+            node = self.uri2object(cr, uid, pool, uri2)
             if not node:
-                raise DAV_NotFound(uri2)
-            if node.type in ('collection','database'):
-                return COLLECTION
-            return OBJECT
+                raise DAV_NotFound2(uri2)
+            try:
+                return node.get_dav_resourcetype(cr)
+            except NotImplementedError:
+                if node.type in ('collection','database'):
+                    return ('collection', 'DAV:')
+                return ''
         finally:
-            cr.close()
+            if cr: cr.close()
 
     def _get_dav_displayname(self,uri):
         self.parent.log_message('get DN: %s' % uri)
-        if uri[-1]=='/':uri=uri[:-1]
         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
         if not dbname:
-            cr.close()
-            return COLLECTION
-        node = self.uri2object(cr,uid,pool, uri2)
+            if cr: cr.close()
+            # at root, dbname, just return the last component
+            # of the path.
+            if uri2 and len(uri2) < 2:
+                return uri2[-1]
+            return ''
+        node = self.uri2object(cr, uid, pool, uri2)
         if not node:
-            cr.close()
-            raise DAV_NotFound(uri2)
+            if cr: cr.close()
+            raise DAV_NotFound2(uri2)
         cr.close()
         return node.displayname
 
     @memoize(CACHE_SIZE)
-    def _get_dav_getcontentlength(self,uri):
-        """ return the content length of an object """
+    def _get_dav_getcontentlength(self, uri):
+        """ return the content length of an object """        
         self.parent.log_message('get length: %s' % uri)
-        if uri[-1]=='/':uri=uri[:-1]
         result = 0
-        cr, uid, pool, dbname, uri2 = self.get_cr(uri)
+        cr, uid, pool, dbname, uri2 = self.get_cr(uri)        
         if not dbname:
-            cr.close()
-            return '0'
+            if cr: cr.close()
+            return str(result)
         node = self.uri2object(cr, uid, pool, uri2)
         if not node:
-            cr.close()
-            raise DAV_NotFound(uri2)
+            if cr: cr.close()
+            raise DAV_NotFound2(uri2)
         result = node.content_length or 0
         cr.close()
         return str(result)
@@ -291,193 +578,179 @@ class tinydav_handler(dav_interface):
     def _get_dav_getetag(self,uri):
         """ return the ETag of an object """
         self.parent.log_message('get etag: %s' % uri)
-        if uri[-1]=='/':uri=uri[:-1]
         result = 0
         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
         if not dbname:
-            cr.close()
+            if cr: cr.close()
             return '0'
         node = self.uri2object(cr, uid, pool, uri2)
         if not node:
             cr.close()
-            raise DAV_NotFound(uri2)
-        result = node.get_etag(cr)
+            raise DAV_NotFound2(uri2)
+        result = self._try_function(node.get_etag ,(cr,), "etag %s" %uri, cr=cr)
         cr.close()
         return str(result)
 
     @memoize(CACHE_SIZE)
-    def get_lastmodified(self,uri):
+    def get_lastmodified(self, uri):
         """ return the last modified date of the object """
-        if uri[-1]=='/':uri=uri[:-1]
-        today = time.time()
         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
-        try:
-            if not dbname:
-                return today
-            node = self.uri2object(cr,uid,pool, uri2)
+        if not dbname:
+            return time.time()
+        try:            
+            node = self.uri2object(cr, uid, pool, uri2)
             if not node:
-                raise DAV_NotFound(uri2)
-            if node.write_date:
-                return time.mktime(time.strptime(node.write_date,'%Y-%m-%d %H:%M:%S'))
-            else:
-                return today
+                raise DAV_NotFound2(uri2)
+            return _str2time(node.write_date)
         finally:
-            cr.close()
+            if cr: cr.close()
 
-    @memoize(CACHE_SIZE)
-    def get_creationdate(self,uri):
-        """ return the last modified date of the object """
+    def _get_dav_getlastmodified(self,uri):
+        """ return the last modified date of a resource
+        """
+        d=self.get_lastmodified(uri)
+        # format it. Note that we explicitly set the day, month names from
+        # an array, so that strftime() doesn't use its own locale-aware
+        # strings.
+        gmt = time.gmtime(d)
+        return time.strftime("%%s, %d %%s %Y %H:%M:%S GMT", gmt ) % \
+                    (day_names[gmt.tm_wday], month_names[gmt.tm_mon])
 
-        if uri[-1]=='/':uri=uri[:-1]
+    @memoize(CACHE_SIZE)
+    def get_creationdate(self, uri):
+        """ return the last modified date of the object """        
         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
-        try:
-            if not dbname:
-                raise DAV_Error, 409
-            node = self.uri2object(cr,uid,pool, uri2)
+        if not dbname:
+            raise DAV_Error, 409
+        try:            
+            node = self.uri2object(cr, uid, pool, uri2)
             if not node:
-                raise DAV_NotFound(uri2)
-            if node.create_date:
-                result = time.strptime(node.create_date,'%Y-%m-%d %H:%M:%S')
-            else:
-                result = time.gmtime()
-            return result
+                raise DAV_NotFound2(uri2)
+
+            return _str2time(node.create_date)
         finally:
-            cr.close()
+            if cr: cr.close()
 
     @memoize(CACHE_SIZE)
     def _get_dav_getcontenttype(self,uri):
         self.parent.log_message('get contenttype: %s' % uri)
-        if uri[-1]=='/':uri=uri[:-1]
         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
-        try:
-            if not dbname:
-                return 'httpd/unix-directory'
-            node = self.uri2object(cr,uid,pool, uri2)
+        if not dbname:
+            if cr: cr.close()
+            return 'httpd/unix-directory'
+        try:            
+            node = self.uri2object(cr, uid, pool, uri2)
             if not node:
-                raise DAV_NotFound(uri2)
-            result = node.mimetype
+                raise DAV_NotFound2(uri2)
+            result = str(node.mimetype)
             return result
             #raise DAV_NotFound, 'Could not find %s' % path
         finally:
-            cr.close()
-
+            if cr: cr.close()    
+    
     def mkcol(self,uri):
-        """ create a new collection """
+        """ create a new collection
+            see par. 9.3 of rfc4918
+        """
         self.parent.log_message('MKCOL: %s' % uri)
-        if uri[-1]=='/':uri=uri[:-1]
-        parent='/'.join(uri.split('/')[:-1])
-        if not parent.startswith(self.baseuri):
-            parent=self.baseuri + ''.join(parent[1:])
-        if not uri.startswith(self.baseuri):
-            uri=self.baseuri + ''.join(uri[1:])
-
-
-        cr, uid, pool,dbname, uri2 = self.get_cr(uri)
+        cr, uid, pool, dbname, uri2 = self.get_cr(uri)
+        if not uri2[-1]:
+            if cr: cr.close()
+            raise DAV_Error(409, "Cannot create nameless collection")
         if not dbname:
+            if cr: cr.close()
             raise DAV_Error, 409
         node = self.uri2object(cr,uid,pool, uri2[:-1])
-        object2 = False            
-        if isinstance(node, node_res_obj):
-            object2 = node and pool.get(node.context.context['res_model']).browse(cr, uid, node.context.context['res_id']) or False            
-        
-        obj = node.context._dirobj.browse(cr, uid, node.dir_id)            
-        if obj and (obj.type == 'ressource') and not object2:
-            raise OSError(1, 'Operation not permited.')
-
-        objname = uri2[-1]
-        val = {
-                'name': objname,
-                'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
-                'ressource_id': object2 and object2.id or False,
-                'parent_id' : False
-        }
-        if (obj and (obj.type in ('directory'))) or not object2:                
-            val['parent_id'] =  obj and obj.id or False            
-        
-        pool.get('document.directory').create(cr, uid, val)
-        cr.commit() 
+        if not node:
+            cr.close()
+            raise DAV_Error(409, "Parent path %s does not exist" % uri2[:-1])
+        nc = node.child(cr, uri2[-1])
+        if nc:
+            cr.close()
+            raise DAV_Error(405, "Path already exists")
+        self._try_function(node.create_child_collection, (cr, uri2[-1]),
+                    "create col %s" % uri2[-1], cr=cr)
+        cr.commit()
         cr.close()
         return True
 
-    def put(self,uri,data,content_type=None):
+    def put(self, uri, data, content_type=None):
         """ put the object into the filesystem """
-        self.parent.log_message('Putting %s (%d), %s'%( misc.ustr(uri), len(data), content_type))
-        parent='/'.join(uri.split('/')[:-1])
+        self.parent.log_message('Putting %s (%d), %s'%( misc.ustr(uri), data and len(data) or 0, content_type))
         cr, uid, pool,dbname, uri2 = self.get_cr(uri)
         if not dbname:
+            if cr: cr.close()
             raise DAV_Forbidden
         try:
-            node = self.uri2object(cr,uid,pool, uri2[:])
-        except:
+            node = self.uri2object(cr, uid, pool, uri2[:])
+        except Exception:
             node = False
-        objname = uri2[-1]
-        ext = objname.find('.') >0 and objname.split('.')[1] or False
-
+        
+        objname = misc.ustr(uri2[-1])
+        
+        ret = None
         if not node:
-            dir_node = self.uri2object(cr,uid,pool, uri2[:-1])
+            dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
             if not dir_node:
+                cr.close()
                 raise DAV_NotFound('Parent folder not found')
+
+            newchild = self._try_function(dir_node.create_child, (cr, objname, data),
+                    "create %s" % objname, cr=cr)
+            if not newchild:
+                cr.commit()
+                cr.close()
+                raise DAV_Error(400, "Failed to create resource")
+            
+            uparts=urlparse.urlparse(uri)
+            fileloc = '/'.join(newchild.full_path())
+            if isinstance(fileloc, unicode):
+                fileloc = fileloc.encode('utf-8')
+            # the uri we get is a mangled one, where the davpath has been removed
+            davpath = self.parent.get_davpath()
+            
+            surl = '%s://%s' % (uparts[0], uparts[1])
+            uloc = urllib.quote(fileloc)
+            hurl = False
+            if uri != ('/'+uloc) and uri != (surl + '/' + uloc):
+                hurl = '%s%s/%s/%s' %(surl, davpath, dbname, uloc)
+            etag = False
             try:
-                dir_node.create_child(cr,objname,data)
-            except Exception,e:
-                import traceback
-                self.parent.log_error("Cannot create %s: %s", objname, str(e))
-                self.parent.log_message("Exc: %s",traceback.format_exc())
-                raise DAV_Forbidden
+                etag = str(newchild.get_etag(cr))
+            except Exception, e:
+                self.parent.log_error("Cannot get etag for node: %s" % e)
+            ret = (str(hurl), etag)
         else:
-            try:
-                node.set_data(cr,data)
-            except Exception,e:
-                import traceback
-                self.parent.log_error("Cannot save %s: %s", objname, str(e))
-                self.parent.log_message("Exc: %s",traceback.format_exc())
-                raise DAV_Forbidden
+            self._try_function(node.set_data, (cr, data), "save %s" % objname, cr=cr)
             
         cr.commit()
         cr.close()
-        return 201
+        return ret
 
     def rmcol(self,uri):
         """ delete a collection """
-        if uri[-1]=='/':uri=uri[:-1]
-
         cr, uid, pool, dbname, uri2 = self.get_cr(uri)        
-        if not dbname: # *-*
+        if not dbname:
+            if cr: cr.close()
             raise DAV_Error, 409
+
         node = self.uri2object(cr, uid, pool, uri2)             
-        object = node.context._dirobj.browse(cr, uid, node.dir_id)
-        
-        if not object:
-            raise OSError(2, 'Not such file or directory.')        
-        if object._table_name=='document.directory':
-            if node.children(cr):
-                raise OSError(39, 'Directory not empty.')
-            res = pool.get('document.directory').unlink(cr, uid, [object.id])
-        else:
-            raise OSError(1, 'Operation not permited.')
+        self._try_function(node.rmcol, (cr,), "rmcol %s" % uri, cr=cr)
 
         cr.commit()
         cr.close()
         return 204
 
     def rm(self,uri):
-        if uri[-1]=='/':uri=uri[:-1]
-
-        object=False
         cr, uid, pool,dbname, uri2 = self.get_cr(uri)
         if not dbname:        
-            cr.close()
+            if cr: cr.close()
             raise DAV_Error, 409
-        node = self.uri2object(cr,uid,pool, uri2)
-        object = pool.get('ir.attachment').browse(cr, uid, node.file_id)
-        self.parent.log_message(' rm %s "%s"'%(object._table_name,uri))
-        if not object:
-            raise OSError(2, 'Not such file or directory.')
-        if object._table_name == 'ir.attachment':
-            res = pool.get('ir.attachment').unlink(cr, uid, [object.id])
-        else:
-            raise OSError(1, 'Operation not permited.')       
-        
+        node = self.uri2object(cr, uid, pool, uri2)
+        res = self._try_function(node.rm, (cr,), "rm %s"  % uri, cr=cr)
+        if not res:
+            if cr: cr.close()
+            raise OSError(1, 'Operation not permited.')        
         cr.commit()
         cr.close()
         return 204
@@ -487,7 +760,7 @@ class tinydav_handler(dav_interface):
     ### a rm directly
     ###
 
-    def delone(self,uri):
+    def delone(self, uri):
         """ delete a single resource
 
         You have to return a result dict of the form
@@ -497,10 +770,10 @@ class tinydav_handler(dav_interface):
         """
         if uri[-1]=='/':uri=uri[:-1]
         res=delone(self,uri)
-        parent='/'.join(uri.split('/')[:-1])
+        # parent='/'.join(uri.split('/')[:-1])
         return res
 
-    def deltree(self,uri):
+    def deltree(self, uri):
         """ delete a collection
 
         You have to return a result dict of the form
@@ -508,8 +781,8 @@ class tinydav_handler(dav_interface):
         or None if everything's ok
         """
         if uri[-1]=='/':uri=uri[:-1]
-        res=deltree(self,uri)
-        parent='/'.join(uri.split('/')[:-1])
+        res=deltree(self, uri)
+        # parent='/'.join(uri.split('/')[:-1])
         return res
 
 
@@ -517,7 +790,7 @@ class tinydav_handler(dav_interface):
     ### MOVE handlers (examples)
     ###
 
-    def moveone(self,src,dst,overwrite):
+    def moveone(self, src, dst, overwrite):
         """ move one resource with Depth=0
 
         an alternative implementation would be
@@ -534,10 +807,10 @@ class tinydav_handler(dav_interface):
         (untested!). This would not use the davcmd functions
         and thus can only detect errors directly on the root node.
         """
-        res=moveone(self,src,dst,overwrite)
+        res=moveone(self, src, dst, overwrite)
         return res
 
-    def movetree(self,src,dst,overwrite):
+    def movetree(self, src, dst, overwrite):
         """ move a collection with Depth=infinity
 
         an alternative implementation would be
@@ -554,14 +827,14 @@ class tinydav_handler(dav_interface):
         (untested!). This would not use the davcmd functions
         and thus can only detect errors directly on the root node"""
 
-        res=movetree(self,src,dst,overwrite)
+        res=movetree(self, src, dst, overwrite)
         return res
 
     ###
     ### COPY handlers
     ###
 
-    def copyone(self,src,dst,overwrite):
+    def copyone(self, src, dst, overwrite):
         """ copy one resource with Depth=0
 
         an alternative implementation would be
@@ -578,10 +851,10 @@ class tinydav_handler(dav_interface):
         (untested!). This would not use the davcmd functions
         and thus can only detect errors directly on the root node.
         """
-        res=copyone(self,src,dst,overwrite)
+        res=copyone(self, src, dst, overwrite)
         return res
 
-    def copytree(self,src,dst,overwrite):
+    def copytree(self, src, dst, overwrite):
         """ copy a collection with Depth=infinity
 
         an alternative implementation would be
@@ -597,7 +870,7 @@ class tinydav_handler(dav_interface):
 
         (untested!). This would not use the davcmd functions
         and thus can only detect errors directly on the root node"""
-        res=copytree(self,src,dst,overwrite)
+        res=copytree(self, src, dst, overwrite)
         return res
 
     ###
@@ -608,15 +881,15 @@ class tinydav_handler(dav_interface):
     ### Look in davcmd.py for further details.
     ###
 
-    def copy(self,src,dst):
+    def copy(self, src, dst):
         src=urllib.unquote(src)
         dst=urllib.unquote(dst)
         ct = self._get_dav_getcontenttype(src)
         data = self.get_data(src)
-        self.put(dst,data,ct)
+        self.put(dst, data, ct)
         return 201
 
-    def copycol(self,src,dst):
+    def copycol(self, src, dst):
         """ copy a collection.
 
         As this is not recursive (the davserver recurses itself)
@@ -624,27 +897,124 @@ class tinydav_handler(dav_interface):
         advanced systems we might also have to copy properties from
         the source to the destination.
         """
-        print " copy a collection."
         return self.mkcol(dst)
 
 
-    def exists(self,uri):
+    def exists(self, uri):
         """ test if a resource exists """
         result = False
         cr, uid, pool,dbname, uri2 = self.get_cr(uri)
         if not dbname:
-            cr.close()
+            if cr: cr.close()
             return True
         try:
-            node = self.uri2object(cr,uid,pool, uri2)
+            node = self.uri2object(cr, uid, pool, uri2)
             if node:
                 result = True
-        except:
+        except Exception:
             pass
         cr.close()
         return result
 
+    def unlock(self, uri, token):
+        """ Unlock a resource from that token 
+        
+        @return True if unlocked, False if no lock existed, Exceptions
+        """
+        cr, uid, pool, dbname, uri2 = self.get_cr(uri)
+        if not dbname:
+            if cr: cr.close()
+            raise DAV_Error, 409
+
+        node = self.uri2object(cr, uid, pool, uri2)
+        try:
+            node_fn = node.dav_unlock
+        except AttributeError:
+            # perhaps the node doesn't support locks
+            cr.close()
+            raise DAV_Error(400, 'No locks for this resource')
+
+        res = self._try_function(node_fn, (cr, token), "unlock %s" % uri, cr=cr)
+        cr.commit()
+        cr.close()
+        return res
+
+    def lock(self, uri, lock_data):
+        """ Lock (may create) resource.
+            Data is a dict, may contain:
+                depth, token, refresh, lockscope, locktype, owner
+        """
+        cr, uid, pool, dbname, uri2 = self.get_cr(uri)
+        created = False
+        if not dbname:
+            if cr: cr.close()
+            raise DAV_Error, 409
+
+        try:
+            node = self.uri2object(cr, uid, pool, uri2[:])
+        except Exception:
+            node = False
+        
+        objname = misc.ustr(uri2[-1])
+        
+        if not node:
+            dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
+            if not dir_node:
+                cr.close()
+                raise DAV_NotFound('Parent folder not found')
+
+            # We create a new node (file) but with empty data=None,
+            # as in RFC4918 p. 9.10.4
+            node = self._try_function(dir_node.create_child, (cr, objname, None),
+                    "create %s" % objname, cr=cr)
+            if not node:
+                cr.commit()
+                cr.close()
+                raise DAV_Error(400, "Failed to create resource")
+            
+            created = True
+
+        try:
+            node_fn = node.dav_lock
+        except AttributeError:
+            # perhaps the node doesn't support locks
+            cr.close()
+            raise DAV_Error(400, 'No locks for this resource')
+
+        # Obtain the lock on the node
+        lres, pid, token = self._try_function(node_fn, (cr, lock_data), "lock %s" % objname, cr=cr)
+
+        if not lres:
+            cr.commit()
+            cr.close()
+            raise DAV_Error(423, "Resource already locked")
+        
+        assert isinstance(lres, list), 'lres: %s' % repr(lres)
+        
+        try:
+            data = mk_lock_response(self, uri, lres)
+            cr.commit()
+        except Exception:
+            cr.close()
+            raise
+
+        cr.close()
+        return created, data, token
+
     @memoize(CACHE_SIZE)
-    def is_collection(self,uri):
+    def is_collection(self, uri):
         """ test if the given uri is a collection """
-        return self._get_dav_resourcetype(uri)==COLLECTION
+        cr, uid, pool, dbname, uri2 = self.get_cr(uri)
+        try:
+            if not dbname:
+                return True
+            node = self.uri2object(cr,uid,pool, uri2)
+            if not node:
+                raise DAV_NotFound2(uri2)
+            if node.type in ('collection','database'):
+                return True
+            return False
+        finally:
+            if cr: cr.close()
+
+#eof