Launchpad automatic translations update.
[odoo/odoo.git] / addons / document / nodes.py
index b849396..1b7aebc 100644 (file)
@@ -61,16 +61,27 @@ def get_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):
@@ -88,10 +99,14 @@ class node_context(object):
 
     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 """
+            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])
@@ -104,14 +119,10 @@ class node_context(object):
         """Create (or locate) a node for a directory
             @param dbro a browse object of document.directory
         """
-        fullpath = self._dirobj.get_full_path(cr, self.uid, dbro.id, self.context)
-        if dbro.type == 'directory':
-            return node_dir(fullpath, None ,self, dbro)
-        elif dbro.type == 'ressource':
-            assert dbro.ressource_parent_type_id == False
-            return node_res_dir(fullpath, None, self, dbro)
-        else:
-            raise ValueError("dir node for %s type", dbro.type)
+        
+        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
@@ -121,7 +132,7 @@ class node_context(object):
         if fbro.parent_id:
             parent = self.get_dir_node(cr, fbro.parent_id)
 
-        return node_file(fbro.name, parent, self, fbro)
+        return self.node_file_class(fbro.name, parent, self, fbro)
 
 
 class node_descriptor(object):
@@ -164,6 +175,23 @@ class node_descriptor(object):
     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.
@@ -266,22 +294,24 @@ 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):
         """ Return the modification time as a unique, compact string """
-        return str(_str2time(self.write_date))
+        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 NotImplementedError("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 """
+        # 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):
@@ -353,12 +383,14 @@ class node_class(object):
     def create_child(self, cr, path, data=None):
         """ Create a regular file under this node
         """
-        raise NotImplementedError(repr(self))
+        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
         """
-        raise NotImplementedError(repr(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 NotImplementedError(repr(self))
@@ -420,12 +452,12 @@ 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))
             is_allowed = self.check_perms(1)
@@ -435,25 +467,14 @@ class node_database(node_class):
         if not is_allowed:
             raise IOError(errno.EPERM, "Permission into directory denied")
 
-        if not domain:
-            domain = []
-
-        where2 = where + domain + [('type', '=', 'directory')]
-        ids = dirobj.search(cr, uid, where2, context=ctx)
+        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))
+            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):
@@ -463,9 +484,6 @@ class node_database(node_class):
     def _get_ttag(self,cr):
         return 'db-%s' % cr.dbname
 
-    def get_dav_resourcetype(self, cr):
-        return ('collection', 'DAV:')
-
 def mkdosname(company_name, default='noname'):
     """ convert a string to a dos-like name"""
     if not company_name:
@@ -477,6 +495,22 @@ def mkdosname(company_name, default='noname'):
     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):
@@ -491,10 +525,13 @@ 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
-        self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
+        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
@@ -548,7 +585,46 @@ class node_dir(node_database):
         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
@@ -670,12 +746,14 @@ class node_dir(node_database):
         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'
+    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
@@ -686,10 +764,13 @@ 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
-        self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
+        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
@@ -743,7 +824,7 @@ class node_res_dir(node_class):
         ctx.update(self.dctx)
         where = []
         if self.domain:
-            app = safe_eval(self.domain, self.dctx)
+            app = safe_eval(self.domain, ctx)
             if not app:
                 pass
             elif isinstance(app, list):
@@ -756,7 +837,9 @@ class node_res_dir(node_class):
             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)
@@ -777,22 +860,30 @@ class node_res_dir(node_class):
         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
 
-    def get_dav_resourcetype(self, cr):
-        return ('collection', 'DAV:')
-
 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).
@@ -809,15 +900,18 @@ 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.unixperms = 040750
         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
-        self.res_find_all = parent.res_find_all
+        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.copy()
@@ -886,7 +980,8 @@ class node_res_obj(node_class):
 
         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
@@ -899,7 +994,8 @@ class node_res_obj(node_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':
             logger.warning("Who asked for %s:%s?" % (ns, prop))
             return None
@@ -910,6 +1006,7 @@ class node_res_obj(node_class):
         where = [('directory_id','=',self.dir_id) ]
         ids = cntobj.search(cr,uid,where,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
@@ -934,54 +1031,72 @@ 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)]
+                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):
-                namefield = directory.resource_field.name or 'name'
                 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')
         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", where2
+        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):
@@ -993,9 +1108,9 @@ class node_res_obj(node_class):
         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:
@@ -1006,7 +1121,8 @@ 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
@@ -1038,7 +1154,8 @@ class node_res_obj(node_class):
 
         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)
+        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
@@ -1046,8 +1163,7 @@ class node_res_obj(node_class):
     def _get_ttag(self,cr):
         return 'rodir-%d-%d' % (self.dir_id, self.res_id)
 
-    def get_dav_resourcetype(self, cr):
-        return ('collection', 'DAV:')
+node_res_dir.res_obj_class = node_res_obj
 
 class node_file(node_class):
     our_type = 'file'
@@ -1069,7 +1185,10 @@ class node_file(node_class):
             elif not parent.check_perms('w'):
                 self.uidperms = 4
     
-        self.uuser = (fil.user_id and fil.user_id.login) or 'nobody'
+        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
@@ -1183,9 +1302,6 @@ class node_file(node_class):
     def _get_ttag(self,cr):
         return 'file-%d' % self.file_id
 
-    def get_dav_resourcetype(self, cr):
-        return ''
-
     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")
@@ -1340,11 +1456,13 @@ class nodefd_content(StringIO, node_descriptor):
     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+'):
@@ -1358,6 +1476,9 @@ class nodefd_content(StringIO, node_descriptor):
             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
@@ -1382,3 +1503,57 @@ class nodefd_content(StringIO, node_descriptor):
         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
+
+        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
\ No newline at end of file