[MERGE]: Merge with lp:~openerp-dev/openobject-addons/trunk-dev-addons1
[odoo/odoo.git] / addons / document / nodes.py
index 822acc3..5222c0e 100644 (file)
 #
 ##############################################################################
 
-import base64
-import StringIO
-from osv import osv, fields
-from osv.orm import except_orm
-import urlparse
+# import urlparse
 import pooler
 from tools.safe_eval import safe_eval
 
-import os
+import errno
+# import os
 import time
+import logging
+
+from StringIO import StringIO
 
 #
 # An object that represent an uri
@@ -41,26 +41,73 @@ import time
 #   root: if we are at the first directory of a ressource
 #
 
+logger = logging.getLogger('doc2.nodes')
+
+def _str2time(cre):
+    """ Convert a string with time representation (from db) into time (float)
+    
+        Note: a place to fix if datetime is used in db.
+    """
+    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
+
 def get_node_context(cr, uid, context):
-    return node_context(cr,uid,context)
+    return node_context(cr, uid, context)
 
 class node_context(object):
-    """ This is the root node, representing access to some particular
-        context """
+    """ This is the root node, representing access to some particular context
+    
+    A context is a set of persistent data, which may influence the structure
+    of the nodes. All other transient information during a data query should
+    be passed down with function arguments.
+    """
     cached_roots = {}
+    node_file_class = None
 
     def __init__(self, cr, uid, context=None):
         self.dbname = cr.dbname
         self.uid = uid
         self.context = context
+        if context is None:
+            context = {}
+        context['uid'] = uid
         self._dirobj = pooler.get_pool(cr.dbname).get('document.directory')
+        self.node_file_class = node_file
+        self.extra_ctx = {} # Extra keys for context, that do _not_ trigger inequality
         assert self._dirobj
+        self._dirobj._prepare_context(cr, uid, self, context=context)
         self.rootdir = False #self._dirobj._get_root_directory(cr,uid,context)
 
+    def __eq__(self, other):
+        if not type(other) == node_context:
+            return False
+        if self.dbname != other.dbname:
+            return False
+        if self.uid != other.uid:
+            return False
+        if self.context != other.context:
+            return False
+        if self.rootdir != other.rootdir:
+            return False
+        return True
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+    
+    def get(self, name, default=None):
+        return self.context.get(name, default)
+
     def get_uri(self, cr,  uri):
         """ Although this fn passes back to doc.dir, it is needed since
-        it is a potential caching point """
-        (ndir, duri) =  self._dirobj._locate_child(cr,self.uid, self.rootdir,uri, None, self)
+            it is a potential caching point.
+        """
+        (ndir, duri) =  self._dirobj._locate_child(cr, self.uid, self.rootdir, uri, None, self)
         while duri:
             ndir = ndir.child(cr, duri[0])
             if not ndir:
@@ -68,12 +115,92 @@ class node_context(object):
             duri = duri[1:]
         return ndir
 
+    def get_dir_node(self, cr, dbro):
+        """Create (or locate) a node for a directory
+            @param dbro a browse object of document.directory
+        """
+        
+        fullpath = dbro.get_full_path(context=self.context)
+        klass = dbro.get_node_class(dbro, context=self.context)
+        return klass(fullpath, None ,self, dbro)
+
+    def get_file_node(self, cr, fbro):
+        """ Create or locate a node for a static file
+            @param fbro a browse object of an ir.attachment
+        """
+        parent = None
+        if fbro.parent_id:
+            parent = self.get_dir_node(cr, fbro.parent_id)
+
+        return self.node_file_class(fbro.name, parent, self, fbro)
+
+
+class node_descriptor(object):
+    """A file-like interface to the data contents of a node.
+
+       This class is NOT a node, but an /open descriptor/ for some
+       node. It can hold references to a cursor or a file object,
+       because the life of a node_descriptor will be the open period
+       of the data.
+       It should also take care of locking, with any native mechanism
+       or using the db.
+       For the implementation, it would be OK just to wrap around file,
+       StringIO or similar class. The node_descriptor is only needed to
+       provide the link to the parent /node/ object.
+    """
+
+    def __init__(self, parent):
+        assert isinstance(parent, node_class)
+        self.name = parent.displayname
+        self.__parent = parent
+
+    def _get_parent(self):
+        return self.__parent
+
+    def open(self, **kwargs):
+        raise NotImplementedError
+
+    def close(self):
+        raise NotImplementedError
+
+    def read(self, size=None):
+        raise NotImplementedError
+
+    def seek(self, offset, whence=None):
+        raise NotImplementedError
+
+    def tell(self):
+        raise NotImplementedError
+
+    def write(self, str):
+        raise NotImplementedError
+
+    def size(self):
+        raise NotImplementedError
+
+    def __len__(self):
+        return self.size()
+
+    def __nonzero__(self):
+        """ Ensure that a node_descriptor will never equal False
+        
+            Since we do define __len__ and __iter__ for us, we must avoid
+            being regarded as non-true objects.
+        """
+        return True
+
+    def next(self, str):
+        raise NotImplementedError
+
 class node_class(object):
     """ this is a superclass for our inodes
         It is an API for all code that wants to access the document files.
         Nodes have attributes which contain usual file properties
         """
     our_type = 'baseclass'
+    DAV_PROPS = None
+    DAV_M_NS = None
+
     def __init__(self, path, parent, context):
         assert isinstance(context,node_context)
         assert (not parent ) or isinstance(parent,node_class)
@@ -81,9 +208,13 @@ class node_class(object):
         self.context = context
         self.type=self.our_type
         self.parent = parent
+        self.uidperms = 5   # computed permissions for our uid, in unix bits
         self.mimetype = 'application/octet-stream'
         self.create_date = None
         self.write_date = None
+        self.unixperms = 0660
+        self.uuser = 'user'
+        self.ugroup = 'group'
         self.content_length = 0
         # dynamic context:
         self.dctx = {}
@@ -91,6 +222,12 @@ class node_class(object):
             self.dctx = parent.dctx.copy()
         self.displayname = 'Object'
 
+    def __eq__(self, other):
+        return NotImplemented
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
     def full_path(self):
         """ Return the components of the full path for some
             node.
@@ -102,9 +239,14 @@ class node_class(object):
             s = []
         if isinstance(self.path,list):
             s+=self.path
+        elif self.path is None:
+            s.append('')
         else:
             s.append(self.path)
         return s #map(lambda x: '/' +x, s)
+        
+    def __repr__(self):
+        return "%s@/%s" % (self.our_type, '/'.join(self.full_path()))
 
     def children(self, cr, domain=None):
         print "node_class.children()"
@@ -114,6 +256,16 @@ class node_class(object):
         print "node_class.child()"
         return None
 
+    def get_uri(self, cr, uri):
+        duri = uri
+        ndir = self
+        while duri:
+            ndir = ndir.child(cr, duri[0])
+            if not ndir:
+                return False
+            duri = duri[1:]
+        return ndir
+
     def path_get(self):
         print "node_class.path_get()"
         return False
@@ -121,6 +273,20 @@ class node_class(object):
     def get_data(self,cr):
         raise TypeError('no data for %s'% self.type)
 
+    def open_data(self, cr, mode):
+        """ Open a node_descriptor object for this node.
+
+        @param the mode of open, eg 'r', 'w', 'a', like file.open()
+
+        This operation may lock the data for this node (and accross
+        other node hierarchies), until the descriptor is close()d. If
+        the node is locked, subsequent opens (depending on mode) may
+        immediately fail with an exception (which?).
+        For this class, there is no data, so no implementation. Each
+        child class that has data should override this.
+        """
+        raise TypeError('no data for %s' % self.type)
+
     def _get_storage(self,cr):
         raise RuntimeError("no storage for base class")
 
@@ -128,26 +294,25 @@ class node_class(object):
         """ Get a tag, unique per object + modification.
 
             see. http://tools.ietf.org/html/rfc2616#section-13.3.3 """
-        return self._get_ttag(cr) + ':' + self._get_wtag(cr)
+        return '"%s-%s"' % (self._get_ttag(cr), self._get_wtag(cr))
 
-    def _get_wtag(self,cr):
+    def _get_wtag(self, cr):
         """ Return the modification time as a unique, compact string """
-        if self.write_date:
-            wtime = time.mktime(time.strptime(self.write_date,'%Y-%m-%d %H:%M:%S'))
-        else: wtime = time.time()
-        return str(wtime)
+        return str(_str2time(self.write_date)).replace('.','')
 
-    def _get_ttag(self,cr):
+    def _get_ttag(self, cr):
         """ Get a unique tag for this type/id of object.
             Must be overriden, so that each node is uniquely identified.
         """
         print "node_class.get_ttag()",self
-        raise RuntimeError("get_etag stub()")
+        raise NotImplementedError("get_ttag stub()")
 
     def get_dav_props(self, cr):
         """ If this class has special behaviour for GroupDAV etc, export
         its capabilities """
-        return {}
+        # This fn is placed here rather than WebDAV, because we want the
+        # baseclass methods to apply to all node subclasses
+        return self.DAV_PROPS or {}
 
     def match_dav_eprop(self, cr, match, ns, prop):
         res = self.get_dav_eprop(cr, ns, prop)
@@ -155,18 +320,115 @@ class node_class(object):
             return True
         return False
 
-    def get_dav_eprop(self,cr,ns,prop):
+    def get_dav_eprop(self, cr, ns, prop):
+        if not self.DAV_M_NS:
+            return None
+        
+        if self.DAV_M_NS.has_key(ns):
+            prefix = self.DAV_M_NS[ns]
+        else:
+            logger.debug('No namespace: %s ("%s")',ns, prop)
+            return None
+
+        mname = prefix + "_" + prop.replace('-','_')
+
+        if not hasattr(self, mname):
+            return None
+
+        try:
+            m = getattr(self, mname)
+            r = m(cr)
+            return r
+        except AttributeError:
+            logger.debug('Property %s not supported' % prop, exc_info=True)
         return None
 
+    def get_dav_resourcetype(self, cr):
+        """ Get the DAV resource type.
+        
+            Is here because some nodes may exhibit special behaviour, like
+            CalDAV/GroupDAV collections
+        """
+        raise NotImplementedError
+
+    def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
+        """ Move this node to a new parent directory.
+        @param ndir_node the collection that this node should be moved under
+        @param new_name a name to rename this node to. If omitted, the old
+            name is preserved
+        @param fil_obj, can be None, is the browse object for the file,
+            if already available.
+        @param ndir_obj must be the browse object to the new doc.directory
+            location, where this node should be moved to.
+        in_write: When called by write(), we shouldn't attempt to write the
+            object, but instead return the dict of vals (avoid re-entrance).
+            If false, we should write all data to the object, here, as if the
+            caller won't do anything after calling move_to()
+
+        Return value:
+            True: the node is moved, the caller can update other values, too.
+            False: the node is either removed or fully updated, the caller
+                must discard the fil_obj, not attempt to write any more to it.
+            dict: values to write back to the object. *May* contain a new id!
+
+        Depending on src and target storage, implementations of this function
+        could do various things.
+        Should also consider node<->content, dir<->dir moves etc.
+
+        Move operations, as instructed from APIs (eg. request from DAV) could
+        use this function.
+        """
+        raise NotImplementedError(repr(self))
+
+    def create_child(self, cr, path, data=None):
+        """ Create a regular file under this node
+        """
+        logger.warning("Attempted to create a file under %r, not possible.", self)
+        raise IOError(errno.EPERM, "Not allowed to create files here")
+    
+    def create_child_collection(self, cr, objname):
+        """ Create a child collection (directory) under self
+        """
+        logger.warning("Attempted to create a collection under %r, not possible.", self)
+        raise IOError(errno.EPERM, "Not allowed to create folders here")
+
     def rm(self, cr):
-        raise RuntimeError("Not Implemented")
+        raise NotImplementedError(repr(self))
 
     def rmcol(self, cr):
-        raise RuntimeError("Not Implemented")
+        raise NotImplementedError(repr(self))
 
     def get_domain(self, cr, filters):
+        # TODO Document
         return []
 
+    def check_perms(self, perms):
+        """ Check the permissions of the current node.
+        
+        @param perms either an integers of the bits to check, or
+                a string with the permission letters
+
+        Permissions of nodes are (in a unix way):
+        1, x : allow descend into dir
+        2, w : allow write into file, or modification to dir
+        4, r : allow read of file, or listing of dir contents
+        8, u : allow remove (unlink)
+        """
+        
+        if isinstance(perms, str):
+            pe2 = 0
+            chars = { 'x': 1, 'w': 2, 'r': 4, 'u': 8 }
+            for c in perms:
+                pe2 = pe2 | chars[c]
+            perms = pe2
+        elif isinstance(perms, int):
+            if perms < 0 or perms > 15:
+                raise ValueError("Invalid permission bits")
+        else:
+            raise ValueError("Invalid permission attribute")
+        
+        return ((self.uidperms & perms) == perms)
+
 class node_database(node_class):
     """ A node representing the database directory
 
@@ -174,6 +436,8 @@ class node_database(node_class):
     our_type = 'database'
     def __init__(self, path=[], parent=False, context=None):
         super(node_database,self).__init__(path, parent, context)
+        self.unixperms = 040750
+        self.uidperms = 5
 
     def children(self, cr, domain=None):
         res = self._child_get(cr, domain=domain) + self._file_get(cr)
@@ -188,57 +452,68 @@ class node_database(node_class):
             return res[0]
         return None
 
-    def _child_get(self, cr, name=False, parent_id=False, domain=None):
+    def _child_get(self, cr, name=False, domain=None):
         dirobj = self.context._dirobj
         uid = self.context.uid
         ctx = self.context.context.copy()
         ctx.update(self.dctx)
-        where = [('parent_id','=',parent_id)]
+        where = [('parent_id','=', False), ('ressource_parent_type_id','=',False)]
         if name:
             where.append(('name','=',name))
-        if not domain:
-            domain = []
-
-        where2 = where + domain + [('type', '=', 'directory')]
-        ids = dirobj.search(cr, uid, where2, context=ctx)
+            is_allowed = self.check_perms(1)
+        else:
+            is_allowed = self.check_perms(5)
+        
+        if not is_allowed:
+            raise IOError(errno.EPERM, "Permission into directory denied")
+
+        if domain:
+            where = where + domain
+        ids = dirobj.search(cr, uid, where, context=ctx)
         res = []
-        for dirr in dirobj.browse(cr,uid,ids,context=ctx):
-            res.append(node_dir(dirr.name,self,self.context,dirr))
-
-        where2 = where + domain + [('type', '=', 'ressource'), ('ressource_parent_type_id','=',False)]
-        ids = dirobj.search(cr, uid, where2, context=ctx)
-        for dirr in dirobj.browse(cr,uid,ids,context=ctx):
-            res.append(node_res_dir(dirr.name,self,self.context,dirr))
+        for dirr in dirobj.browse(cr, uid, ids, context=ctx):
+            klass = dirr.get_node_class(dirr, context=ctx)
+            res.append(klass(dirr.name, self, self.context,dirr))
 
-        fil_obj = dirobj.pool.get('ir.attachment')
-        ids = fil_obj.search(cr,uid,where,context=ctx)
-        if ids:
-            for fil in fil_obj.browse(cr,uid,ids,context=ctx):
-                res.append(node_file(fil.name,self,self.context,fil))
         return res
 
-    def _file_get(self,cr, nodename=False, directory_id=False):
+    def _file_get(self,cr, nodename=False):
         res = []
-        cntobj = self.context._dirobj.pool.get('document.directory.content')
-        uid = self.context.uid
-        ctx = self.context.context.copy()
-        ctx.update(self.dctx)
-        where = [('directory_id','=',directory_id) ]
-        ids = cntobj.search(cr, uid, where, context=ctx)
-        for content in cntobj.browse(cr, uid, ids, context=ctx):
-            res3 = cntobj._file_get(cr, self, nodename, content)
-            if res3:
-                res.extend(res3)
-
         return res
 
     def _get_ttag(self,cr):
         return 'db-%s' % cr.dbname
 
+def mkdosname(company_name, default='noname'):
+    """ convert a string to a dos-like name"""
+    if not company_name:
+        return default
+    badchars = ' !@#$%^`~*()+={}[];:\'"/?.<>'
+    n = ''
+    for c in company_name[:8]:
+        n += (c in badchars and '_') or c
+    return n
+    
+
+def _uid2unixperms(perms, has_owner):
+    """ Convert the uidperms and the owner flag to full unix bits
+    """
+    res = 0
+    if has_owner:
+        res |= (perms & 0x07) << 6
+        res |= (perms & 0x05) << 3
+    elif perms & 0x02:
+        res |= (perms & 0x07) << 6
+        res |= (perms & 0x07) << 3
+    else:
+        res |= (perms & 0x07) << 6
+        res |= (perms & 0x05) << 3
+        res |= 0x05
+    return res
 
 class node_dir(node_database):
     our_type = 'collection'
-    def __init__(self,path, parent, context, dirr, dctx=None):
+    def __init__(self, path, parent, context, dirr, dctx=None):
         super(node_dir,self).__init__(path, parent,context)
         self.dir_id = dirr and dirr.id or False
         #todo: more info from dirr
@@ -250,6 +525,16 @@ class node_dir(node_database):
         # TODO: the write date should be MAX(file.write)..
         self.write_date = dirr and (dirr.write_date or dirr.create_date) or False
         self.content_length = 0
+
+        self.unixperms = 040750
+
+        try:
+            self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
+        except Exception:
+            self.uuser = 'nobody'
+        self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
+        self.uidperms = dirr.get_dir_permissions()
+        self.unixperms = 040000 | _uid2unixperms(self.uidperms, dirr and dirr.user_id)
         if dctx:
             self.dctx.update(dctx)
         dc2 = self.context.context
@@ -265,20 +550,84 @@ class node_dir(node_database):
                     print e
                     pass
 
+    def __eq__(self, other):
+        if type(self) != type(other):
+            return False
+        if not self.context == other.context:
+            return False
+        # Two directory nodes, for the same document.directory, may have a
+        # different context! (dynamic folders)
+        if self.dctx != other.dctx:
+            return False
+        return self.dir_id == other.dir_id
 
-    def get_data(self,cr):
-        res = ''
-        for child in self.children(cr):
-            res += child.get_data(cr)
-        return res
-
-    def _file_get(self,cr, nodename=False):
-        return super(node_dir,self)._file_get(cr, nodename, self.dir_id)
+    def get_data(self, cr):
+        #res = ''
+        #for child in self.children(cr):
+        #    res += child.get_data(cr)
+        return None
 
+    def _file_get(self, cr, nodename=False):
+        res = super(node_dir,self)._file_get(cr, nodename)
+        
+        is_allowed = self.check_perms(nodename and 1 or 5)
+        if not is_allowed:
+            raise IOError(errno.EPERM, "Permission into directory denied")
 
+        cntobj = self.context._dirobj.pool.get('document.directory.content')
+        uid = self.context.uid
+        ctx = self.context.context.copy()
+        ctx.update(self.dctx)
+        where = [('directory_id','=',self.dir_id) ]
+        ids = cntobj.search(cr, uid, where, context=ctx)
+        for content in cntobj.browse(cr, uid, ids, context=ctx):
+            res3 = cntobj._file_get(cr, self, nodename, content)
+            if res3:
+                res.extend(res3)
 
+        return res
+    
     def _child_get(self, cr, name=None, domain=None):
-        return super(node_dir,self)._child_get(cr, name, self.dir_id, domain=domain)
+        dirobj = self.context._dirobj
+        uid = self.context.uid
+        ctx = self.context.context.copy()
+        ctx.update(self.dctx)
+        where = [('parent_id','=',self.dir_id)]
+        if name:
+            where.append(('name','=',name))
+            is_allowed = self.check_perms(1)
+        else:
+            is_allowed = self.check_perms(5)
+        
+        if not is_allowed:
+            raise IOError(errno.EPERM, "Permission into directory denied")
+
+        if not domain:
+            domain = []
+
+        where2 = where + domain + [('ressource_parent_type_id','=',False)]
+        ids = dirobj.search(cr, uid, where2, context=ctx)
+        res = []
+        for dirr in dirobj.browse(cr, uid, ids, context=ctx):
+            klass = dirr.get_node_class(dirr, context=ctx)
+            res.append(klass(dirr.name, self, self.context,dirr))
+
+        # Static directories should never return files with res_model/res_id
+        # because static dirs are /never/ related to a record.
+        # In fact, files related to some model and parented by the root dir
+        # (the default), will NOT be accessible in the node system unless
+        # a resource folder for that model exists (with resource_find_all=True).
+        # Having resource attachments in a common folder is bad practice,
+        # because they would be visible to all users, and their names may be
+        # the same, conflicting.
+        where += [('res_model', '=', False)]
+        fil_obj = dirobj.pool.get('ir.attachment')
+        ids = fil_obj.search(cr, uid, where, context=ctx)
+        if ids:
+            for fil in fil_obj.browse(cr, uid, ids, context=ctx):
+                klass = self.context.node_file_class
+                res.append(klass(fil.name, self, self.context, fil))
+        return res
 
     def rmcol(self, cr):
         uid = self.context.uid
@@ -286,6 +635,9 @@ class node_dir(node_database):
         res = False
         if not directory:
             raise OSError(2, 'Not such file or directory.')
+        if not self.check_perms('u'):
+            raise IOError(errno.EPERM,"Permission denied")
+
         if directory._table_name=='document.directory':
             if self.children(cr):
                 raise OSError(39, 'Directory not empty.')
@@ -296,6 +648,9 @@ class node_dir(node_database):
 
     def create_child_collection(self, cr, objname):
         object2 = False
+        if not self.check_perms(2):
+            raise IOError(errno.EPERM,"Permission denied")
+
         dirobj = self.context._dirobj
         uid = self.context.uid
         ctx = self.context.context.copy()
@@ -315,10 +670,13 @@ class node_dir(node_database):
         return dirobj.create(cr, uid, val)
 
 
-    def create_child(self,cr,path,data):
+    def create_child(self, cr, path, data=None):
         """ API function to create a child file object and node
             Return the node_* created
         """
+        if not self.check_perms(2):
+            raise IOError(errno.EPERM,"Permission denied")
+
         dirobj = self.context._dirobj
         uid = self.context.uid
         ctx = self.context.context.copy()
@@ -331,37 +689,76 @@ class node_dir(node_database):
             # Datas are not set here
         }
 
-        fil_id = fil_obj.create(cr,uid, val, context=ctx)
-        fil = fil_obj.browse(cr,uid,fil_id,context=ctx)
-        fnode = node_file(path,self,self.context,fil)
-        fnode.set_data(cr,data,fil)
+        fil_id = fil_obj.create(cr, uid, val, context=ctx)
+        fil = fil_obj.browse(cr, uid, fil_id, context=ctx)
+        fnode = node_file(path, self, self.context, fil)
+        if data is not None:
+            fnode.set_data(cr, data, fil)
         return fnode
 
-    def get_etag(self, cr):
-        """ Get a tag, unique per object + modification.
+    def _get_ttag(self,cr):
+        return 'dir-%d' % self.dir_id
 
-            see. http://tools.ietf.org/html/rfc2616#section-13.3.3 """
-        return self._get_ttag(cr) + ':' + self._get_wtag(cr)
+    def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
+        """ Move directory. This operation is simple, since the present node is
+        only used for static, simple directories.
+            Note /may/ be called with ndir_node = None, to rename the document root.
+        """
+        if ndir_node and (ndir_node.context != self.context):
+            raise NotImplementedError("Cannot move directories between contexts")
 
-    def _get_wtag(self, cr):
-        """ Return the modification time as a unique, compact string """
-        if self.write_date:
-            wtime = time.mktime(time.strptime(self.write_date, '%Y-%m-%d %H:%M:%S'))
-        else: wtime = time.time()
-        return str(wtime)
+        if (not self.check_perms('u')) or (not ndir_node.check_perms('w')):
+            raise IOError(errno.EPERM,"Permission denied")
 
-    def _get_ttag(self,cr):
-        return 'dir-%d' % self.dir_id
+        dir_obj = self.context._dirobj
+        if not fil_obj:
+            dbro = dir_obj.browse(cr, self.context.uid, self.dir_id, context=self.context.context)
+        else:
+            dbro = dir_obj
+            assert dbro.id == self.dir_id
+
+        if not dbro:
+            raise IndexError("Cannot locate dir %d", self.dir_id)
+
+        if (not self.parent) and ndir_node:
+            if not dbro.parent_id:
+                raise IOError(errno.EPERM, "Cannot move the root directory!")
+            self.parent = self.context.get_dir_node(cr, dbro.parent_id)
+            assert self.parent
+
+        if self.parent != ndir_node:
+            logger.debug('Cannot move dir %r from %r to %r', self, self.parent, ndir_node)
+            raise NotImplementedError('Cannot move dir to another dir')
+
+        ret = {}
+        if new_name and (new_name != dbro.name):
+            if ndir_node.child(cr, new_name):
+                raise IOError(errno.EEXIST, "Destination path already exists")
+            ret['name'] = new_name
+
+        del dbro
+
+        if not in_write:
+            # We have to update the data ourselves
+            if ret:
+                ctx = self.context.context.copy()
+                ctx['__from_node'] = True
+                dir_obj.write(cr, self.context.uid, [self.dir_id,], ret, ctx)
+            ret = True
+
+        return ret
 
 class node_res_dir(node_class):
-    """ A special sibling to node_dir, which does only contain dynamically
+    """ A folder containing dynamic folders
+        A special sibling to node_dir, which does only contain dynamically
         created folders foreach resource in the foreign model.
         All folders should be of type node_res_obj and merely behave like
         node_dirs (with limited domain).
     """
     our_type = 'collection'
-    def __init__(self,path, parent, context, dirr, dctx=None ):
-        super(node_res_dir,self).__init__(path, parent,context)
+    res_obj_class = None
+    def __init__(self, path, parent, context, dirr, dctx=None ):
+        super(node_res_dir,self).__init__(path, parent, context)
         self.dir_id = dirr.id
         #todo: more info from dirr
         self.mimetype = 'application/x-directory'
@@ -370,8 +767,17 @@ class node_res_dir(node_class):
         # TODO: the write date should be MAX(file.write)..
         self.write_date = dirr.write_date or dirr.create_date
         self.content_length = 0
+        self.unixperms = 040750
+        try:
+            self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
+        except Exception:
+            self.uuser = 'nobody'
+        self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
+        self.uidperms = dirr.get_dir_permissions()
+        self.unixperms = 040000 | _uid2unixperms(self.uidperms, dirr and dirr.user_id)
         self.res_model = dirr.ressource_type_id and dirr.ressource_type_id.model or False
         self.resm_id = dirr.ressource_id
+        self.res_find_all = dirr.resource_find_all
         self.namefield = dirr.resource_field.name or 'name'
         self.displayname = dirr.name
         # Important: the domain is evaluated using the *parent* dctx!
@@ -386,6 +792,17 @@ class node_res_dir(node_class):
         for dfld in dirr.dctx_ids:
             self.dctx_dict['dctx_' + dfld.field] = dfld.expr
 
+    def __eq__(self, other):
+        if type(self) != type(other):
+            return False
+        if not self.context == other.context:
+            return False
+        # Two nodes, for the same document.directory, may have a
+        # different context! (dynamic folders)
+        if self.dctx != other.dctx:
+            return False
+        return self.dir_id == other.dir_id
+
     def children(self, cr, domain=None):
         return self._child_get(cr, domain=domain)
 
@@ -395,7 +812,7 @@ class node_res_dir(node_class):
             return res[0]
         return None
 
-    def _child_get(self,cr, name = None, domain=None):
+    def _child_get(self, cr, name = None, domain=None):
         """ return virtual children of resource, based on the
             foreign object.
 
@@ -404,7 +821,6 @@ class node_res_dir(node_class):
         """
         obj = self.context._dirobj.pool.get(self.res_model)
         if not obj:
-            print "couldn't find model", self.res_model
             return []
         dirobj = self.context._dirobj
         uid = self.context.uid
@@ -412,12 +828,28 @@ class node_res_dir(node_class):
         ctx.update(self.dctx)
         where = []
         if self.domain:
-            where += safe_eval(self.domain, self.dctx)
+            app = safe_eval(self.domain, ctx)
+            if not app:
+                pass
+            elif isinstance(app, list):
+                where.extend(app)
+            elif isinstance(app, tuple):
+                where.append(app)
+            else:
+                raise RuntimeError("incorrect domain expr: %s" % self.domain)
         if self.resm_id:
             where.append(('id','=',self.resm_id))
 
         if name:
-            where.append((self.namefield,'=',name))
+            # The =like character will match underscores against any characters
+            # including the special ones that couldn't exist in a FTP/DAV request
+            where.append((self.namefield,'=like',name.replace('\\','\\\\')))
+            is_allowed = self.check_perms(1)
+        else:
+            is_allowed = self.check_perms(5)
+
+        if not is_allowed:
+            raise IOError(errno.EPERM,"Permission denied")
 
         # print "Where clause for %s" % self.res_model, where
         if self.ressource_tree:
@@ -427,24 +859,35 @@ class node_res_dir(node_class):
             if obj._parent_name in obj.fields_get(cr, uid):
                 where.append((obj._parent_name,'=',object2 and object2.id or False))
 
-        resids = obj.search(cr,uid, where, context=ctx)
+        resids = obj.search(cr, uid, where, context=ctx)
         res = []
-        for bo in obj.browse(cr,uid,resids,context=ctx):
+        for bo in obj.browse(cr, uid, resids, context=ctx):
             if not bo:
                 continue
-            name = getattr(bo,self.namefield)
-            if not name:
+            res_name = getattr(bo, self.namefield)
+            if not res_name:
                 continue
                 # Yes! we can't do better but skip nameless records.
+            
+            # Escape the name for characters not supported in filenames
+            res_name = res_name.replace('/','_') # any other weird char?
+            
+            if name and (res_name != name):
+                # we have matched _ to any character, but we only meant to match
+                # the special ones.
+                # Eg. 'a_c' will find 'abc', 'a/c', 'a_c', may only
+                # return 'a/c' and 'a_c'
+                continue
 
-            res.append(node_res_obj(name, self.dir_id, self, self.context, self.res_model, bo))
+            res.append(self.res_obj_class(res_name, self.dir_id, self, self.context, self.res_model, bo))
         return res
 
     def _get_ttag(self,cr):
         return 'rdir-%d' % self.dir_id
 
 class node_res_obj(node_class):
-    """ A special sibling to node_dir, which does only contain dynamically
+    """ A dynamically created folder.
+        A special sibling to node_dir, which does only contain dynamically
         created folders foreach resource in the foreign model.
         All folders should be of type node_res_obj and merely behave like
         node_dirs (with limited domain).
@@ -461,20 +904,28 @@ class node_res_obj(node_class):
         # TODO: the write date should be MAX(file.write)..
         self.write_date = parent.write_date
         self.content_length = 0
+        self.uidperms = parent.uidperms & 15
+        self.unixperms = 040000 | _uid2unixperms(self.uidperms, True)
+        self.uuser = parent.uuser
+        self.ugroup = parent.ugroup
         self.res_model = res_model
         self.domain = parent.domain
         self.displayname = path
         self.dctx_dict = parent.dctx_dict
+        if isinstance(parent, node_res_dir):
+            self.res_find_all = parent.res_find_all
+        else:
+            self.res_find_all = False
         if res_bo:
             self.res_id = res_bo.id
-            dc2 = self.context.context
+            dc2 = self.context.context.copy()
             dc2.update(self.dctx)
             dc2['res_model'] = res_model
             dc2['res_id'] = res_bo.id
             dc2['this'] = res_bo
             for fld,expr in self.dctx_dict.items():
                 try:
-                    self.dctx[fld] = safe_eval(expr,dc2)
+                    self.dctx[fld] = safe_eval(expr, dc2)
                 except Exception,e:
                     print "Cannot eval %s for %s" % (expr, fld)
                     print e
@@ -482,23 +933,41 @@ class node_res_obj(node_class):
         else:
             self.res_id = res_id
 
+    def __eq__(self, other):
+        if type(self) != type(other):
+            return False
+        if not self.context == other.context:
+            return False
+        if not self.res_model == other.res_model:
+            return False
+        if not self.res_id == other.res_id:
+            return False
+        if self.domain != other.domain:
+            return False
+        if self.res_find_all != other.res_find_all:
+            return False
+        if self.dctx != other.dctx:
+            return False
+        return self.dir_id == other.dir_id
+
     def children(self, cr, domain=None):
         return self._child_get(cr, domain=domain) + self._file_get(cr)
 
-    def child(self,cr, name):
-        res = self._child_get(cr,name)
-
-    def child(self,cr, name, domain=None):
+    def child(self, cr, name, domain=None):
         res = self._child_get(cr, name, domain=domain)
         if res:
             return res[0]
-        res = self._file_get(cr,name)
+        res = self._file_get(cr, name)
         if res:
             return res[0]
         return None
 
     def _file_get(self,cr, nodename=False):
         res = []
+        is_allowed = self.check_perms((nodename and 1) or 5)
+        if not is_allowed:
+            raise IOError(errno.EPERM,"Permission denied")
+
         cntobj = self.context._dirobj.pool.get('document.directory.content')
         uid = self.context.uid
         ctx = self.context.context.copy()
@@ -507,45 +976,52 @@ class node_res_obj(node_class):
         #if self.domain:
         #    where.extend(self.domain)
         # print "res_obj file_get clause", where
-        ids = cntobj.search(cr,uid,where,context=ctx)
-        for content in cntobj.browse(cr,uid,ids,context=ctx):
-            res3 = cntobj._file_get(cr,self,nodename,content, context=ctx)
+        ids = cntobj.search(cr, uid, where, context=ctx)
+        for content in cntobj.browse(cr, uid, ids, context=ctx):
+            res3 = cntobj._file_get(cr, self, nodename, content, context=ctx)
             if res3:
                 res.extend(res3)
 
         return res
 
-    def get_dav_props(self, cr):
+    def get_dav_props_DEPR(self, cr):
+        # Deprecated! (but document_ics must be cleaned, first)
         res = {}
         cntobj = self.context._dirobj.pool.get('document.directory.content')
         uid = self.context.uid
         ctx = self.context.context.copy()
         ctx.update(self.dctx)
         where = [('directory_id','=',self.dir_id) ]
-        ids = cntobj.search(cr,uid,where,context=ctx)
-        for content in cntobj.browse(cr,uid,ids,context=ctx):
+        ids = cntobj.search(cr, uid, where, context=ctx)
+        for content in cntobj.browse(cr, uid, ids, context=ctx):
             if content.extension == '.ics': # FIXME: call the content class!
                 res['http://groupdav.org/'] = ('resourcetype',)
         return res
 
-    def get_dav_eprop(self,cr,ns,prop):
+    def get_dav_eprop_DEPR(self, cr, ns, prop):
+        # Deprecated!
         if ns != 'http://groupdav.org/' or prop != 'resourcetype':
-            print "Who asked for %s:%s?" % (ns,prop)
+            logger.warning("Who asked for %s:%s?" % (ns, prop))
             return None
-        res = {}
         cntobj = self.context._dirobj.pool.get('document.directory.content')
         uid = self.context.uid
         ctx = self.context.context.copy()
         ctx.update(self.dctx)
         where = [('directory_id','=',self.dir_id) ]
         ids = cntobj.search(cr,uid,where,context=ctx)
-        for content in cntobj.browse(cr,uid,ids,context=ctx):
+        for content in cntobj.browse(cr, uid, ids, context=ctx):
+            # TODO: remove relic of GroupDAV
             if content.extension == '.ics': # FIXME: call the content class!
                 return ('vevent-collection','http://groupdav.org/')
         return None
 
-    def _child_get(self,cr, name=None, domain=None):
+    def _child_get(self, cr, name=None, domain=None):
         dirobj = self.context._dirobj
+
+        is_allowed = self.check_perms((name and 1) or 5)
+        if not is_allowed:
+            raise IOError(errno.EPERM,"Permission denied")
+
         uid = self.context.uid
         ctx = self.context.context.copy()
         ctx.update(self.dctx)
@@ -559,62 +1035,86 @@ class node_res_obj(node_class):
         # Directory Structure display in tree structure
         if self.res_id and directory.ressource_tree:
             where1 = []
+            if name:
+                where1.append(('name','=like',name.replace('\\','\\\\')))
             if obj._parent_name in obj.fields_get(cr, uid):
-                where1 = where + [(obj._parent_name, '=', self.res_id)]
-            resids = obj.search(cr,uid, where1, context=ctx)
-            for bo in obj.browse(cr,uid,resids,context=ctx):
-                namefield = directory.resource_field.name or 'name'
+                where1.append((obj._parent_name, '=', self.res_id))
+            namefield = directory.resource_field.name or 'name'
+            resids = obj.search(cr, uid, where1, context=ctx)
+            for bo in obj.browse(cr, uid, resids, context=ctx):
                 if not bo:
                     continue
                 res_name = getattr(bo, namefield)
                 if not res_name:
                     continue
-                res.append(node_res_obj(res_name, self.dir_id, self, self.context, self.res_model, res_bo = bo))
+                res_name = res_name.replace('/', '_')
+                if name and (res_name != name):
+                    continue
+                # TODO Revise
+                klass = directory.get_node_class(directory, dynamic=True, context=ctx)
+                rnode = klass(res_name, dir_id=self.dir_id, parent=self, context=self.context,
+                                res_model=self.res_model, res_bo=bo)
+                rnode.res_find_all = self.res_find_all
+                res.append(rnode)
 
 
         where2 = where + [('parent_id','=',self.dir_id) ]
         ids = dirobj.search(cr, uid, where2, context=ctx)
+        bo = obj.browse(cr, uid, self.res_id, context=ctx)
+        
         for dirr in dirobj.browse(cr, uid, ids, context=ctx):
+            if name and (name != dirr.name):
+                continue
             if dirr.type == 'directory':
-                res.append(node_res_obj(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id))
+                klass = dirr.get_node_class(dirr, dynamic=True, context=ctx)
+                res.append(klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = bo, res_id = self.res_id))
             elif dirr.type == 'ressource':
                 # child resources can be controlled by properly set dctx
-                res.append(node_res_dir(dirr.name,self,self.context, dirr, {'active_id': self.res_id}))
-
-
-
+                klass = dirr.get_node_class(dirr, context=ctx)
+                res.append(klass(dirr.name,self,self.context, dirr, {'active_id': self.res_id})) # bo?
 
         fil_obj = dirobj.pool.get('ir.attachment')
-        where3 = where2  + [('res_model', '=', self.res_model), ('res_id','=',self.res_id)]
-        # print "where clause for dir_obj", where2
+        if self.res_find_all:
+            where2 = where
+        where3 = where2 + [('res_model', '=', self.res_model), ('res_id','=',self.res_id)]
+        # print "where clause for dir_obj", where3
         ids = fil_obj.search(cr, uid, where3, context=ctx)
         if ids:
             for fil in fil_obj.browse(cr, uid, ids, context=ctx):
-                res.append(node_file(fil.name, self, self.context, fil))
+                klass = self.context.node_file_class
+                res.append(klass(fil.name, self, self.context, fil))
 
 
         # Get Child Ressource Directories
         if directory.ressource_type_id and directory.ressource_type_id.id:
             where4 = where + [('ressource_parent_type_id','=',directory.ressource_type_id.id)]
-            where5 = where4 + [('ressource_id','=',0)]
+            where5 = where4 + ['|', ('ressource_id','=',0), ('ressource_id','=',self.res_id)]
             dirids = dirobj.search(cr,uid, where5)
-            where5 = where4 + [('ressource_id','=',self.res_id)]
-            dirids = dirids + dirobj.search(cr,uid, where5)
             for dirr in dirobj.browse(cr, uid, dirids, context=ctx):
                 if dirr.type == 'directory' and not dirr.parent_id:
-                    res.append(node_res_obj(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id))
+                    klass = dirr.get_node_class(dirr, dynamic=True, context=ctx)
+                    rnode = klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = bo, res_id = self.res_id)
+                    rnode.res_find_all = dirr.resource_find_all
+                    res.append(rnode)
                 if dirr.type == 'ressource':
-                    res.append(node_res_dir(dirr.name, self, self.context, dirr, {'active_id': self.res_id}))
+                    klass = dirr.get_node_class(dirr, context=ctx)
+                    rnode = klass(dirr.name, self, self.context, dirr, {'active_id': self.res_id})
+                    rnode.res_find_all = dirr.resource_find_all
+                    res.append(rnode)
         return res
 
     def create_child_collection(self, cr, objname):
         dirobj = self.context._dirobj
+        is_allowed = self.check_perms(2)
+        if not is_allowed:
+            raise IOError(errno.EPERM,"Permission denied")
+
         uid = self.context.uid
         ctx = self.context.context.copy()
         ctx.update(self.dctx)
-        res_obj = dirobj.pool.get(self.context.context['res_model'])
+        res_obj = dirobj.pool.get(self.res_model)
 
-        object2 = res_obj.browse(cr, uid, self.context.context['res_id']) or False
+        object2 = res_obj.browse(cr, uid, self.res_id) or False
 
         obj = dirobj.browse(cr, uid, self.dir_id)
         if obj and (obj.type == 'ressource') and not object2:
@@ -625,17 +1125,22 @@ class node_res_obj(node_class):
                 '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
+                'parent_id' : False,
+                'resource_find_all': False,
         }
         if (obj and (obj.type in ('directory'))) or not object2:
             val['parent_id'] =  obj and obj.id or False
 
         return dirobj.create(cr, uid, val)
 
-    def create_child(self,cr,path,data):
+    def create_child(self, cr, path, data=None):
         """ API function to create a child file object and node
             Return the node_* created
         """
+        is_allowed = self.check_perms(2)
+        if not is_allowed:
+            raise IOError(errno.EPERM,"Permission denied")
+
         dirobj = self.context._dirobj
         uid = self.context.uid
         ctx = self.context.context.copy()
@@ -644,55 +1149,87 @@ class node_res_obj(node_class):
         val = {
             'name': path,
             'datas_fname': path,
-            'parent_id': self.dir_id,
             'res_model': self.res_model,
             'res_id': self.res_id,
             # Datas are not set here
         }
-
-        fil_id = fil_obj.create(cr,uid, val, context=ctx)
-        fil = fil_obj.browse(cr,uid,fil_id,context=ctx)
-        fnode = node_file(path,self,self.context,fil)
-        fnode.set_data(cr,data,fil)
+        if not self.res_find_all:
+            val['parent_id'] = self.dir_id
+
+        fil_id = fil_obj.create(cr, uid, val, context=ctx)
+        fil = fil_obj.browse(cr, uid, fil_id, context=ctx)
+        klass = self.context.node_file_class
+        fnode = klass(path, self, self.context, fil)
+        if data is not None:
+            fnode.set_data(cr, data, fil)
         return fnode
 
     def _get_ttag(self,cr):
-        return 'rodir-%d-%d' % (self.dir_id,self.res_id)
+        return 'rodir-%d-%d' % (self.dir_id, self.res_id)
+
+node_res_dir.res_obj_class = node_res_obj
 
 class node_file(node_class):
     our_type = 'file'
-    def __init__(self,path, parent, context, fil):
+    def __init__(self, path, parent, context, fil):
         super(node_file,self).__init__(path, parent,context)
         self.file_id = fil.id
         #todo: more info from ir_attachment
         if fil.file_type and '/' in fil.file_type:
-            self.mimetype = fil.file_type
+            self.mimetype = str(fil.file_type)
         self.create_date = fil.create_date
         self.write_date = fil.write_date or fil.create_date
         self.content_length = fil.file_size
         self.displayname = fil.name
+        
+        self.uidperms = 14
+        if parent:
+            if not parent.check_perms('x'):
+                self.uidperms = 0
+            elif not parent.check_perms('w'):
+                self.uidperms = 4
+    
+        try:
+            self.uuser = (fil.user_id and fil.user_id.login) or 'nobody'
+        except Exception:
+            self.uuser = 'nobody'
+        self.ugroup = mkdosname(fil.company_id and fil.company_id.name, default='nogroup')
 
         # This only propagates the problem to get_data. Better
         # fix those files to point to the root dir.
-        if fil.parent_id:
-            self.storage_id = fil.parent_id.storage_id.id
-        else:
-            self.storage_id = None
-
-    def open(self, cr, mode=False):
-        uid = self.context.uid
-        if self.type in ('collection','database'):
+        self.storage_id = None
+        par = fil.parent_id
+        while par:
+            if par.storage_id and par.storage_id.id:
+                self.storage_id = par.storage_id.id
+                break
+            par = par.parent_id
+
+    def __eq__(self, other):
+        if type(self) != type(other):
             return False
-        fobj = self.context._dirobj.pool.get('ir.attachment').browse(cr, uid, self.file_id, context=self.context.context)
-        if fobj.store_method and fobj.store_method== 'fs' :
-            s = StringIO.StringIO(self.get_data(cr, fobj))
-        else:
-            s = StringIO.StringIO(base64.decodestring(fobj.db_datas or ''))
-        s.name = self
-        return s
+        if not self.context == other.context:
+            return False
+        if self.dctx != other.dctx:
+            return False
+        return self.file_id == other.file_id
+
+
+    def open_data(self, cr, mode):
+        stor = self.storage_id
+        assert stor, "No storage for file #%s" % self.file_id
+        if not self.check_perms(4):
+            raise IOError(errno.EPERM, "Permission denied")
+
+        # If storage is not set properly, we are just screwed here, don't
+        # try to get it from default.
+        stobj = self.context._dirobj.pool.get('document.storage')
+        return stobj.get_file(cr, self.context.uid, stor, self, mode=mode, context=self.context.context)
 
     def rm(self, cr):
         uid = self.context.uid
+        if not self.check_perms(8):
+            raise IOError(errno.EPERM, "Permission denied")
         document_obj = self.context._dirobj.pool.get('ir.attachment')
         if self.type in ('collection','database'):
             return False
@@ -736,14 +1273,14 @@ class node_file(node_class):
             the browse object. """
         # this is where storage kicks in..
         stor = self.storage_id
-        if not stor:
-            data_obj = self.context._dirobj.pool.get('ir.model.data')
-            data_id = data_obj._get_id(cr, self.context.uid, 'document', 'storage_db')
-            if data_id:
-                stor = data_obj.browse(cr, self.context.uid, data_id, context=self.context.context).res_id
-        assert stor
+        assert stor, "No storage for file #%s" % self.file_id
+        if not self.check_perms(4):
+            raise IOError(errno.EPERM, "Permission denied")
+
+        # If storage is not set properly, we are just screwed here, don't
+        # try to get it from default.
         stobj = self.context._dirobj.pool.get('document.storage')
-        return stobj.get_data(cr,self.context.uid,stor, self,self.context.context, fil_obj)
+        return stobj.get_data(cr, self.context.uid,stor, self,self.context.context, fil_obj)
 
     def get_data_len(self, cr, fil_obj = None):
         # TODO: verify with the storage object!
@@ -759,26 +1296,88 @@ class node_file(node_class):
             the browse object. """
         # this is where storage kicks in..
         stor = self.storage_id
-        if not stor:
-            data_obj = self.context._dirobj.pool.get('ir.model.data')
-            data_id = data_obj._get_id(cr, self.context.uid, 'document', 'storage_db')
-            if data_id:
-                stor = data_obj.browse(cr, self.context.uid, data_id, context=self.context.context).res_id
-        assert stor
+        assert stor, "No storage for file #%s" % self.file_id
+        if not self.check_perms(2):
+            raise IOError(errno.EPERM, "Permission denied")
+
         stobj = self.context._dirobj.pool.get('document.storage')
-        return stobj.set_data(cr,self.context.uid,stor, self, data, self.context.context, fil_obj)
+        return stobj.set_data(cr, self.context.uid,stor, self, data, self.context.context, fil_obj)
 
     def _get_ttag(self,cr):
         return 'file-%d' % self.file_id
 
+    def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
+        if ndir_node and ndir_node.context != self.context:
+            raise NotImplementedError("Cannot move files between contexts")
+
+        if (not self.check_perms(8)) and ndir_node.check_perms(2):
+            raise IOError(errno.EPERM, "Permission denied")
+
+        doc_obj = self.context._dirobj.pool.get('ir.attachment')
+        if not fil_obj:
+            dbro = doc_obj.browse(cr, self.context.uid, self.file_id, context=self.context.context)
+        else:
+            dbro = fil_obj
+            assert dbro.id == self.file_id, "%s != %s for %r" % (dbro.id, self.file_id, self)
+
+        if not dbro:
+            raise IndexError("Cannot locate doc %d", self.file_id)
+
+        if (not self.parent):
+            # there *must* be a parent node for this one
+            self.parent = self.context.get_dir_node(cr, dbro.parent_id)
+            assert self.parent
+        
+        ret = {}
+        if ndir_node and self.parent != ndir_node:
+            if not (isinstance(self.parent, node_dir) and isinstance(ndir_node, node_dir)):
+                logger.debug('Cannot move file %r from %r to %r', self, self.parent, ndir_node)
+                raise NotImplementedError('Cannot move files between dynamic folders')
+
+            if not ndir_obj:
+                ndir_obj = self.context._dirobj.browse(cr, self.context.uid, \
+                        ndir_node.dir_id, context=self.context.context)
+
+            assert ndir_obj.id == ndir_node.dir_id
+
+            stobj = self.context._dirobj.pool.get('document.storage')
+            r2 = stobj.simple_move(cr, self.context.uid, self, ndir_obj, \
+                        context=self.context.context)
+            ret.update(r2)
+
+        if new_name and (new_name != dbro.name):
+            if len(ret):
+                raise NotImplementedError("Cannot rename and move") # TODO
+            stobj = self.context._dirobj.pool.get('document.storage')
+            r2 = stobj.simple_rename(cr, self.context.uid, self, new_name, self.context.context)
+            ret.update(r2)
+
+        del dbro
+
+        if not in_write:
+            # We have to update the data ourselves
+            if ret:
+                ctx = self.context.context.copy()
+                ctx['__from_node'] = True
+                doc_obj.write(cr, self.context.uid, [self.file_id,], ret, ctx )
+            ret = True
+
+        return ret
+
 class node_content(node_class):
     our_type = 'content'
-    def __init__(self,path, parent, context, cnt, dctx = None, act_id=None):
+    def __init__(self, path, parent, context, cnt, dctx = None, act_id=None):
         super(node_content,self).__init__(path, parent,context)
         self.cnt_id = cnt.id
         self.create_date = False
         self.write_date = False
         self.content_length = False
+        self.unixperms = 0640
+        if parent:
+            self.uidperms = parent.uidperms & 14
+            self.uuser = parent.uuser
+            self.ugroup = parent.ugroup
+        
         self.extension = cnt.extension
         self.report_id = cnt.report_id and cnt.report_id.id
         #self.mimetype = cnt.extension.
@@ -787,17 +1386,7 @@ class node_content(node_class):
            self.dctx.update(dctx)
         self.act_id = act_id
 
-    def open(self, cr, mode=False):
-        uid = self.context.uid
-        if self.type in ('collection','database'):
-            return False
-        pool = self.context._dirobj.pool
-        res = getattr(pool.get('document.directory.content'), 'process_read')(cr, uid, self)
-        res = StringIO.StringIO(res)
-        res.name = self
-        return res
-
-    def fill_fields(self,cr,dctx = None):
+    def fill_fields(self, cr, dctx = None):
         """ Try to read the object and fill missing fields, like mimetype,
             dates etc.
             This function must be different from the constructor, because
@@ -808,174 +1397,167 @@ class node_content(node_class):
                 (self.extension,))
         res = cr.fetchall()
         if res and res[0][0]:
-            self.mimetype = res[0][0]
+            self.mimetype = str(res[0][0])
 
 
     def get_data(self, cr, fil_obj = None):
         cntobj = self.context._dirobj.pool.get('document.directory.content')
+        if not self.check_perms(4):
+            raise IOError(errno.EPERM, "Permission denied")
+
         ctx = self.context.context.copy()
         ctx.update(self.dctx)
-        data = cntobj.process_read(cr,self.context.uid,self,ctx)
+        data = cntobj.process_read(cr, self.context.uid, self, ctx)
         if data:
             self.content_length = len(data)
         return data
 
+    def open_data(self, cr, mode):
+        if mode.endswith('b'):
+            mode = mode[:-1]
+        if mode in ('r', 'w'):
+            cperms = mode[:1]
+        elif mode in ('r+', 'w+'):
+            cperms = 'rw'
+        else:
+            raise IOError(errno.EINVAL, "Cannot open at mode %s" % mode)
+        
+        if not self.check_perms(cperms):
+            raise IOError(errno.EPERM, "Permission denied")
+
+        ctx = self.context.context.copy()
+        ctx.update(self.dctx)
+        
+        return nodefd_content(self, cr, mode, ctx)
+
     def get_data_len(self, cr, fil_obj = None):
+        # FIXME : here, we actually generate the content twice!!
+        # we should have cached the generated content, but it is
+        # not advisable to do keep it in memory, until we have a cache
+        # expiration logic.
         if not self.content_length:
             self.get_data(cr,fil_obj)
         return self.content_length
 
     def set_data(self, cr, data, fil_obj = None):
         cntobj = self.context._dirobj.pool.get('document.directory.content')
+        if not self.check_perms(2):
+            raise IOError(errno.EPERM, "Permission denied")
+
         ctx = self.context.context.copy()
         ctx.update(self.dctx)
-        return cntobj.process_write(cr,self.context.uid,self, data,ctx)
+        return cntobj.process_write(cr, self.context.uid, self, data, ctx)
 
     def _get_ttag(self,cr):
         return 'cnt-%d%s' % (self.cnt_id,(self.act_id and ('-' + str(self.act_id))) or '')
 
-class old_class(osv.osv):
-    # the old code, remove..
-    def __init__(self, cr, uid, path, object, object2=False, context={}, content=False, type='collection', root=False):
-        self.cr = cr
-    def _file_get(self, nodename=False):
-        if not self.object:
-            return []
-        pool = pooler.get_pool(self.cr.dbname)
-        fobj = pool.get('ir.attachment')
-        res2 = []
-        where = []
-        if self.object2:
-            where.append( ('res_model','=',self.object2._name) )
-            where.append( ('res_id','=',self.object2.id) )
-        else:
-            where.append( ('parent_id','=',self.object.id) )
-            where.append( ('res_id','=',False) )
-        if nodename:
-            where.append( (fobj._rec_name,'=',nodename) )
-        for content in self.object.content_ids:
-            res3 = content._table._file_get(self,nodename,content)
-            if res3:
-                res2.extend(res3)
-
-        ids = fobj.search(self.cr, self.uid, where+[ ('parent_id','=',self.object and self.object.id or False) ])
-        if self.object and self.root and (self.object.type=='ressource'):
-            ids += fobj.search(self.cr, self.uid, where+[ ('parent_id','=',False) ])
-        res = fobj.browse(self.cr, self.uid, ids, context=self.context)
-        return map(lambda x: node_class(self.cr, self.uid, self.path+'/'+eval('x.'+fobj._rec_name), x, False, context=self.context, type='file', root=False), res) + res2
-
-    def get_translation(self,value,lang):
-        # Must go, it works on arbitrary models and could be ambiguous.
-        result = value
-        pool = pooler.get_pool(self.cr.dbname)
-        translation_ids = pool.get('ir.translation').search(self.cr, self.uid, [('value','=',value),('lang','=',lang),('type','=','model')])
-        if len(translation_ids):
-            tran_id = translation_ids[0]
-            translation = pool.get('ir.translation').read(self.cr, self.uid, tran_id, ['res_id','name'])
-            res_model,field_name = tuple(translation['name'].split(','))
-            res_id = translation['res_id']
-            res = pool.get(res_model).read(self.cr, self.uid, res_id, [field_name])
-            if res:
-                result = res[field_name]
-        return result
-
-    def directory_list_for_child(self,nodename,parent=False):
-        pool = pooler.get_pool(self.cr.dbname)
-        where = []
-        if nodename:
-            nodename = self.get_translation(nodename, self.context['lang'])
-            where.append(('name','=',nodename))
-        if (self.object and self.object.type=='directory') or not self.object2:
-            where.append(('parent_id','=',self.object and self.object.id or False))
-        else:
-            where.append(('parent_id','=',False))
-        if self.object:
-            where.append(('ressource_parent_type_id','=',self.object.ressource_type_id.id))
-        else:
-            where.append(('ressource_parent_type_id','=',False))
-
-        ids = pool.get('document.directory').search(self.cr, self.uid, where+[('ressource_id','=',0)])
-        if self.object2:
-            ids += pool.get('document.directory').search(self.cr, self.uid, where+[('ressource_id','=',self.object2.id)])
-        res = pool.get('document.directory').browse(self.cr, self.uid, ids, self.context)
-        return res
-
-    def _child_get(self, nodename=False):
-        if self.type not in ('collection','database'):
-            return []
-        res = self.directory_list_for_child(nodename)
-        result= map(lambda x: node_class(self.cr, self.uid, self.path+'/'+x.name, x, x.type=='directory' and self.object2 or False, context=self.context, root=self.root), res)
-        if self.type=='database':
-            pool = pooler.get_pool(self.cr.dbname)
-            fobj = pool.get('ir.attachment')
-            vargs = [('parent_id','=',False),('res_id','=',False)]
-            if nodename:
-                vargs.append((fobj._rec_name,'=',nodename))
-            file_ids=fobj.search(self.cr,self.uid,vargs)
-
-            res = fobj.browse(self.cr, self.uid, file_ids, context=self.context)
-            result +=map(lambda x: node_class(self.cr, self.uid, self.path+'/'+eval('x.'+fobj._rec_name), x, False, context=self.context, type='file', root=self.root), res)
-        if self.type=='collection' and self.object.type=="ressource":
-            where = self.object.domain and eval(self.object.domain, {'active_id':self.root, 'uid':self.uid}) or []
-            pool = pooler.get_pool(self.cr.dbname)
-            obj = pool.get(self.object.ressource_type_id.model)
-            _dirname_field = obj._rec_name
-            if len(obj.fields_get(self.cr, self.uid, ['dirname'])):
-                _dirname_field = 'dirname'
-
-            name_for = obj._name.split('.')[-1]
-            if nodename  and nodename.find(name_for) == 0  :
-                id = int(nodename.replace(name_for,''))
-                where.append(('id','=',id))
-            elif nodename:
-                if nodename.find('__') :
-                    nodename=nodename.replace('__','/')
-                for invalid in INVALID_CHARS:
-                    if nodename.find(INVALID_CHARS[invalid]) :
-                        nodename=nodename.replace(INVALID_CHARS[invalid],invalid)
-                nodename = self.get_translation(nodename, self.context['lang'])
-                where.append((_dirname_field,'=',nodename))
-
-            if self.object.ressource_tree:
-                if obj._parent_name in obj.fields_get(self.cr,self.uid):
-                    where.append((obj._parent_name,'=',self.object2 and self.object2.id or False))
-                    ids = obj.search(self.cr, self.uid, where)
-                    res = obj.browse(self.cr, self.uid, ids,self.context)
-                    result+= map(lambda x: node_class(self.cr, self.uid, self.path+'/'+x.name.replace('/','__'), self.object, x, context=self.context, root=x.id), res)
-                    return result
-                else :
-                    if self.object2:
-                        return result
-            else:
-                if self.object2:
-                    return result
-
-
-            ids = obj.search(self.cr, self.uid, where)
-            res = obj.browse(self.cr, self.uid, ids,self.context)
-            for r in res:
-                if len(obj.fields_get(self.cr, self.uid, [_dirname_field])):
-                    r.name = eval('r.'+_dirname_field)
-                else:
-                    r.name = False
-                if not r.name:
-                    r.name = name_for + '%d'%r.id
-                for invalid in INVALID_CHARS:
-                    if r.name.find(invalid) :
-                        r.name = r.name.replace(invalid,INVALID_CHARS[invalid])
-            result2 = map(lambda x: node_class(self.cr, self.uid, self.path+'/'+x.name.replace('/','__'), self.object, x, context=self.context, root=x.id), res)
-            if result2:
-                if self.object.ressource_tree:
-                    result += result2
-                else:
-                    result = result2
-        return result
+    def get_dav_resourcetype(self, cr):
+        return ''
 
+class nodefd_content(StringIO, node_descriptor):
+    """ A descriptor to content nodes
+    """
+    def __init__(self, parent, cr, mode, ctx):
+        node_descriptor.__init__(self, parent)
+        self._context=ctx
+        self._size = 0L
+
+        if mode in ('r', 'r+'):
+            cntobj = parent.context._dirobj.pool.get('document.directory.content')
+            data = cntobj.process_read(cr, parent.context.uid, parent, ctx)
+            if data:
+                self._size = len(data)
+                parent.content_length = len(data)
+            StringIO.__init__(self, data)
+        elif mode in ('w', 'w+'):
+            StringIO.__init__(self, None)
+            # at write, we start at 0 (= overwrite), but have the original
+            # data available, in case of a seek()
+        elif mode == 'a':
+            StringIO.__init__(self, None)
+        else:
+            logging.getLogger('document.content').error("Incorrect mode %s specified", mode)
+            raise IOError(errno.EINVAL, "Invalid file mode")
+        self.mode = mode
+
+    def size(self):
+        return self._size
+
+    def close(self):
+        # we now open a *separate* cursor, to update the data.
+        # FIXME: this may be improved, for concurrency handling
+        if self.mode == 'r':
+            StringIO.close(self)
+            return
 
-    def path_get(self):
-        path = self.path
-        if self.path[0]=='/':
-            path = self.path[1:]
-        return path
+        par = self._get_parent()
+        uid = par.context.uid
+        cr = pooler.get_db(par.context.dbname).cursor()
+        try:
+            if self.mode in ('w', 'w+', 'r+'):
+                data = self.getvalue()
+                cntobj = par.context._dirobj.pool.get('document.directory.content')
+                cntobj.process_write(cr, uid, par, data, par.context.context)
+            elif self.mode == 'a':
+                raise NotImplementedError
+            cr.commit()
+        except Exception:
+            logging.getLogger('document.content').exception('Cannot update db content #%d for close:', par.cnt_id)
+            raise
+        finally:
+            cr.close()
+        StringIO.close(self)
+
+class nodefd_static(StringIO, node_descriptor):
+    """ A descriptor to nodes with static data.
+    """
+    def __init__(self, parent, cr, mode, ctx=None):
+        node_descriptor.__init__(self, parent)
+        self._context=ctx
+        self._size = 0L
+
+        if mode in ('r', 'r+'):
+            data = parent.get_data(cr)
+            if data:
+                self._size = len(data)
+                parent.content_length = len(data)
+            StringIO.__init__(self, data)
+        elif mode in ('w', 'w+'):
+            StringIO.__init__(self, None)
+            # at write, we start at 0 (= overwrite), but have the original
+            # data available, in case of a seek()
+        elif mode == 'a':
+            StringIO.__init__(self, None)
+        else:
+            logging.getLogger('document.nodes').error("Incorrect mode %s specified", mode)
+            raise IOError(errno.EINVAL, "Invalid file mode")
+        self.mode = mode
+
+    def size(self):
+        return self._size
+
+    def close(self):
+        # we now open a *separate* cursor, to update the data.
+        # FIXME: this may be improved, for concurrency handling
+        if self.mode == 'r':
+            StringIO.close(self)
+            return
 
-old_class()
+        par = self._get_parent()
+        # uid = par.context.uid
+        cr = pooler.get_db(par.context.dbname).cursor()
+        try:
+            if self.mode in ('w', 'w+', 'r+'):
+                data = self.getvalue()
+                par.set_data(cr, data)
+            elif self.mode == 'a':
+                raise NotImplementedError
+            cr.commit()
+        except Exception:
+            logging.getLogger('document.nodes').exception('Cannot update db content #%d for close:', par.cnt_id)
+            raise
+        finally:
+            cr.close()
+        StringIO.close(self)
+
+#eof