diff options
Diffstat (limited to 'addons/document')
| -rw-r--r-- | addons/document/__openerp__.py | 2 | ||||
| -rw-r--r-- | addons/document/board_document_view.xml | 10 | ||||
| -rw-r--r-- | addons/document/content_index.py | 5 | ||||
| -rw-r--r-- | addons/document/directory_content.py | 21 | ||||
| -rw-r--r-- | addons/document/document.py | 141 | ||||
| -rw-r--r-- | addons/document/document_data.xml | 7 | ||||
| -rw-r--r-- | addons/document/document_directory.py | 54 | ||||
| -rw-r--r-- | addons/document/document_storage.py | 505 | ||||
| -rw-r--r-- | addons/document/document_view.xml | 101 | ||||
| -rw-r--r-- | addons/document/nodes.py | 858 | ||||
| -rw-r--r-- | addons/document/report/document_report_view.xml | 22 | ||||
| -rw-r--r-- | addons/document/test/document_test2.yml | 72 |
12 files changed, 1446 insertions, 352 deletions
diff --git a/addons/document/__openerp__.py b/addons/document/__openerp__.py index f8a489a941c..f97cf7ec275 100644 --- a/addons/document/__openerp__.py +++ b/addons/document/__openerp__.py @@ -53,7 +53,7 @@ ], 'demo_xml': [ 'document_demo.xml','board_document_demo.xml'], 'test': [ - 'test/document_test.yml', + 'test/document_test2.yml', ], 'installable': True, 'active': False, diff --git a/addons/document/board_document_view.xml b/addons/document/board_document_view.xml index 0b85dc8039c..8b8ef2557e6 100644 --- a/addons/document/board_document_view.xml +++ b/addons/document/board_document_view.xml @@ -53,7 +53,7 @@ <menuitem id="menu_reporting" name="Reporting" sequence="2" parent="knowledge.menu_document"/> <menuitem name="Dashboard" - id="menu_board_document" + id="menu_reports_document" parent="menu_reporting" sequence="0" icon="terp-graph" @@ -61,10 +61,10 @@ <menuitem - parent="menu_board_document" + parent="menu_reports_document" action="open_board_document_manager" sequence="1" - id="menu_board_document_manager" + id="menu_reports_document_manager" icon="terp-graph"/> <record model="ir.ui.view" id="board_document_manager_form1"> @@ -107,10 +107,10 @@ </record> <menuitem - parent="menu_board_document" + parent="menu_reports_document" action="open_board_document_manager1" sequence="1" - id="menu_board_document_manager1" + id="menu_reports_document_manager1" icon="terp-graph"/> </data> </openerp> diff --git a/addons/document/content_index.py b/addons/document/content_index.py index e7f88834647..d6012e60091 100644 --- a/addons/document/content_index.py +++ b/addons/document/content_index.py @@ -153,7 +153,10 @@ class contentIndex(object): if realfname : fname = realfname else: - bname,ext = os.path.splitext(filename) + try: + bname,ext = os.path.splitext(filename or 'test.tmp') + except Exception: + bname, ext = filename, 'tmp' fd, fname = tempfile.mkstemp(suffix=ext) os.write(fd, content) os.close(fd) diff --git a/addons/document/directory_content.py b/addons/document/directory_content.py index 2d434712f56..bec4f25e45d 100644 --- a/addons/document/directory_content.py +++ b/addons/document/directory_content.py @@ -58,7 +58,9 @@ class document_directory_content(osv.osv): 'suffix': fields.char('Suffix', size=16), 'report_id': fields.many2one('ir.actions.report.xml', 'Report'), 'extension': fields.selection(_extension_get, 'Document Type', required=True, size=4), - 'include_name': fields.boolean('Include Record Name', help="Check this field if you want that the name of the file start by the record name."), + 'include_name': fields.boolean('Include Record Name', + help="Check this field if you want that the name of the file to contain the record name." \ + "\nIf set, the directory will have to be a resource one."), 'directory_id': fields.many2one('document.directory', 'Directory'), } _defaults = { @@ -88,12 +90,19 @@ class document_directory_content(osv.osv): tname = (content.prefix or '') + (content.suffix or '') + (content.extension or '') if tname.find('/'): tname=tname.replace('/', '_') + act_id = False + if 'dctx_res_id' in node.dctx: + act_id = node.dctx['dctx_res_id'] + elif hasattr(node, 'res_id'): + act_id = node.res_id + else: + act_id = node.context.context.get('res_id',False) if not nodename: - n = nodes.node_content(tname, node, node.context,content) + n = nodes.node_content(tname, node, node.context,content, act_id=act_id) res2.append( n) else: if nodename == tname: - n = nodes.node_content(tname, node, node.context,content) + n = nodes.node_content(tname, node, node.context,content, act_id=act_id) n.fill_fields(cr) res2.append(n) return res2 @@ -108,6 +117,10 @@ class document_directory_content(osv.osv): raise Exception("Invalid content: %s" % node.extension) report = self.pool.get('ir.actions.report.xml').browse(cr, uid, node.report_id) srv = netsvc.Service._services['report.'+report.report_name] - pdf,pdftype = srv.create(cr, uid, [node.context.context['res_id']], {}, {}) + ctx = node.context.context.copy() + ctx.update(node.dctx) + pdf,pdftype = srv.create(cr, uid, [node.act_id,], {}, context=ctx) return pdf document_directory_content() + +#eof
\ No newline at end of file diff --git a/addons/document/document.py b/addons/document/document.py index aa893b617c3..c62f62da2d9 100644 --- a/addons/document/document.py +++ b/addons/document/document.py @@ -28,6 +28,7 @@ import os import pooler import netsvc +from osv.orm import except_orm #import StringIO from psycopg2 import Binary @@ -46,7 +47,10 @@ class document_file(osv.osv): def _data_get(self, cr, uid, ids, name, arg, context): fbrl = self.browse(cr, uid, ids, context=context) - nctx = nodes.get_node_context(cr, uid, context) + nctx = nodes.get_node_context(cr, uid, context={}) + # nctx will /not/ inherit the caller's context. Most of + # it would be useless, anyway (like active_id, active_model, + # bin_size etc.) result = {} bin_size = context.get('bin_size', False) for fbro in fbrl: @@ -66,39 +70,38 @@ class document_file(osv.osv): if not value: return True fbro = self.browse(cr, uid, id, context=context) - nctx = nodes.get_node_context(cr, uid, context) + nctx = nodes.get_node_context(cr, uid, context={}) fnode = nodes.node_file(None, None, nctx, fbro) res = fnode.set_data(cr, base64.decodestring(value), fbro) return res _columns = { - 'user_id': fields.many2one('res.users', 'Owner', select=1), - 'group_ids': fields.many2many('res.groups', 'document_group_rel', 'item_id', 'group_id', 'Groups'), - # the directory id now is mandatory. It can still be computed automatically. - 'parent_id': fields.many2one('document.directory', 'Directory', select=1), - 'file_size': fields.integer('File Size', required=True), - 'file_type': fields.char('Content Type', size=128), + # Columns from ir.attachment: + 'create_date': fields.datetime('Date Created', readonly=True), + 'create_uid': fields.many2one('res.users', 'Creator', readonly=True), + 'write_date': fields.datetime('Date Modified', readonly=True), + 'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True), + 'res_model': fields.char('Attached Model', size=64, readonly=True), + 'res_id': fields.integer('Attached ID', readonly=True), + # If ir.attachment contained any data before document is installed, preserve # the data, don't drop the column! 'db_datas': fields.binary('Data', oldname='datas'), - 'index_content': fields.text('Indexed Content'), - 'write_date': fields.datetime('Date Modified', readonly=True), - 'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True), - 'create_date': fields.datetime('Date Created', readonly=True), - 'create_uid': fields.many2one('res.users', 'Creator', readonly=True), - 'store_method': fields.selection([('db', 'Database'), ('fs', 'Filesystem'), ('link', 'Link')], "Storing Method"), 'datas': fields.function(_data_get, method=True, fnct_inv=_data_set, string='File Content', type="binary", nodrop=True), - 'url': fields.char('File URL',size=64), - 'store_fname': fields.char('Stored Filename', size=200), - 'res_model': fields.char('Attached Model', size=64), #res_model - 'res_id': fields.integer('Attached ID'), #res_id - 'partner_id':fields.many2one('res.partner', 'Partner', select=1), - 'type':fields.selection([ - ('url','URL'), - ('binary','Binary'), - ],'Type', help="Type is used to separate URL and binary File"), + # Fields of document: + 'user_id': fields.many2one('res.users', 'Owner', select=1), + # 'group_ids': fields.many2many('res.groups', 'document_group_rel', 'item_id', 'group_id', 'Groups'), + # the directory id now is mandatory. It can still be computed automatically. + 'parent_id': fields.many2one('document.directory', 'Directory', select=1, required=True), + 'index_content': fields.text('Indexed Content'), + 'partner_id':fields.many2one('res.partner', 'Partner', select=1), 'company_id': fields.many2one('res.company', 'Company'), + 'file_size': fields.integer('File Size', required=True), + 'file_type': fields.char('Content Type', size=128), + + # fields used for file storage + 'store_fname': fields.char('Stored Filename', size=200), } def __get_def_directory(self, cr, uid, context=None): @@ -109,8 +112,6 @@ class document_file(osv.osv): 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'ir.attachment', context=c), 'user_id': lambda self, cr, uid, ctx:uid, 'file_size': lambda self, cr, uid, ctx:0, - 'store_method': lambda *args: 'db', - 'type': 'binary', 'parent_id': __get_def_directory } _sql_constraints = [ @@ -122,7 +123,7 @@ class document_file(osv.osv): res_model = vals.get('res_model', False) res_id = vals.get('res_id', 0) if op == 'write': - for file in self.browse(cr, uid, ids): + for file in self.browse(cr, uid, ids): # FIXME fields_only if not name: name = file.name if not parent_id: @@ -156,8 +157,44 @@ class document_file(osv.osv): return False if not self._check_duplication(cr, uid, vals, ids, 'write'): raise osv.except_osv(_('ValidateError'), _('File name must be unique!')) - result = super(document_file, self).write(cr, uid, ids, vals, context=context) - cr.commit() + + # if nodes call this write(), they must skip the code below + from_node = context and context.get('__from_node', False) + if (('parent_id' in vals) or ('name' in vals)) and not from_node: + # perhaps this file is renaming or changing directory + nctx = nodes.get_node_context(cr,uid,context={}) + dirobj = self.pool.get('document.directory') + if 'parent_id' in vals: + dbro = dirobj.browse(cr, uid, vals['parent_id'], context=context) + dnode = nctx.get_dir_node(cr, dbro) + else: + dbro = None + dnode = None + ids2 = [] + result = False + for fbro in self.browse(cr, uid, ids, context=context): + if ('parent_id' not in vals or fbro.parent_id.id == vals['parent_id']) \ + and ('name' not in vals or fbro.name == vals['name']) : + ids2.append(fbro.id) + continue + fnode = nctx.get_file_node(cr, fbro) + res = fnode.move_to(cr, dnode or fnode.parent, vals.get('name', fbro.name), fbro, dbro, True) + if isinstance(res, dict): + vals2 = vals.copy() + vals2.update(res) + wid = res.get('id', fbro.id) + result = super(document_file,self).write(cr,uid,wid,vals2,context=context) + # TODO: how to handle/merge several results? + elif res == True: + ids2.append(fbro.id) + elif res == False: + pass + ids = ids2 + if 'file_size' in vals: # only write that field using direct SQL calls + del vals['file_size'] + if len(ids) and len(vals): + result = super(document_file,self).write(cr, uid, ids, vals, context=context) + cr.commit() # ? return result def create(self, cr, uid, vals, context=None): @@ -170,26 +207,10 @@ class document_file(osv.osv): vals['res_id'] = context.get('default_res_id', False) if not vals.get('res_model', False) and context.get('default_res_model', False): vals['res_model'] = context.get('default_res_model', False) - if vals.get('res_id', False) and vals.get('res_model', False): - obj_model = self.pool.get(vals['res_model']) - result = obj_model.read(cr, uid, [vals['res_id']], ['name', 'partner_id', 'address_id'], context=context) - if len(result): - obj = result[0] - if obj_model._name == 'res.partner': - vals['partner_id'] = obj['id'] - elif obj.get('address_id', False): - if isinstance(obj['address_id'], tuple) or isinstance(obj['address_id'], list): - address_id = obj['address_id'][0] - else: - address_id = obj['address_id'] - address = self.pool.get('res.partner.address').read(cr, uid, [address_id], context=context) - if len(address): - vals['partner_id'] = address[0]['partner_id'][0] or False - elif obj.get('partner_id', False): - if isinstance(obj['partner_id'], tuple) or isinstance(obj['partner_id'], list): - vals['partner_id'] = obj['partner_id'][0] - else: - vals['partner_id'] = obj['partner_id'] + if vals.get('res_id', False) and vals.get('res_model', False) \ + and not vals.get('partner_id', False): + vals['partner_id'] = self.__get_partner_id(cr, uid, \ + vals['res_model'], vals['res_id'], context) datas = None if vals.get('link', False) : @@ -198,13 +219,33 @@ class document_file(osv.osv): else: datas = vals.get('datas', False) - vals['file_size'] = datas and len(datas) or 0 + if datas: + vals['file_size'] = len(datas) + else: + if vals.get('file_size'): + del vals['file_size'] if not self._check_duplication(cr, uid, vals): raise osv.except_osv(_('ValidateError'), _('File name must be unique!')) result = super(document_file, self).create(cr, uid, vals, context) - cr.commit() + cr.commit() # ? return result + def __get_partner_id(self, cr, uid, res_model, res_id, context): + """ A helper to retrieve the associated partner from any res_model+id + It is a hack that will try to discover if the mentioned record is + clearly associated with a partner record. + """ + obj_model = self.pool.get(res_model) + if obj_model._name == 'res.partner': + return res_id + elif 'partner_id' in obj_model._columns and obj_model._columns['partner_id']._obj == 'res.partner': + bro = obj_model.browse(cr, uid, res_id, context=context) + return bro.partner_id.id + elif 'address_id' in obj_model._columns and obj_model._columns['address_id']._obj == 'res.partner.address': + bro = obj_model.browse(cr, uid, res_id, context=context) + return bro.address_id.partner_id.id + return False + def unlink(self, cr, uid, ids, context={}): stor = self.pool.get('document.storage') unres = [] diff --git a/addons/document/document_data.xml b/addons/document/document_data.xml index c2bef5de34f..f13213034e8 100644 --- a/addons/document/document_data.xml +++ b/addons/document/document_data.xml @@ -9,7 +9,7 @@ </record> <record model="document.storage" id="storage_default"> - <field name="name">Default DB storage</field> + <field name="name">Database storage</field> <field name="type">db</field> <field name="user_id" ref="base.user_admin"/> </record> @@ -17,7 +17,7 @@ <record model="document.directory" id="dir_root"> <field name="name">Documents</field> <field name="user_id" ref="base.user_admin"/> - <field name="storage_id" ref="storage_default"/> + <field name="storage_id" ref="storage_default"/> <field name="ressource_id">0</field> </record> @@ -25,7 +25,6 @@ <field name="name">My Folder</field> <field name="parent_id" ref="dir_root"/> <field name="user_id" ref="base.user_admin"/> - <field name="storage_id" ref="storage_default"/> <field name="ressource_id">0</field> </record> @@ -35,7 +34,6 @@ <field name="parent_id" ref="dir_root"/> <field name="type">ressource</field> <field name="ressource_tree">1</field> - <field name="storage_id" ref="storage_default"/> <field name="ressource_id">0</field> <field name="ressource_type_id" search="[('model','=','res.partner.category')]" /> @@ -57,7 +55,6 @@ <field name="name">Personal Folders</field> <field name="parent_id" ref="dir_root"/> <field name="type">ressource</field> - <field name="storage_id" ref="storage_default"/> <field name="ressource_type_id" ref="base.model_res_users" /> <field name="ressource_id">0</field> diff --git a/addons/document/document_directory.py b/addons/document/document_directory.py index fee633b7169..f5b9202fc8e 100644 --- a/addons/document/document_directory.py +++ b/addons/document/document_directory.py @@ -39,20 +39,27 @@ class document_directory(osv.osv): 'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True), 'create_date': fields.datetime('Date Created', readonly=True), 'create_uid': fields.many2one('res.users', 'Creator', readonly=True), - 'file_type': fields.char('Content Type', size=32), 'domain': fields.char('Domain', size=128, help="Use a domain if you want to apply an automatic filter on visible resources."), 'user_id': fields.many2one('res.users', 'Owner'), 'storage_id': fields.many2one('document.storage', 'Storage'), 'group_ids': fields.many2many('res.groups', 'document_directory_group_rel', 'item_id', 'group_id', 'Groups'), - 'parent_id': fields.many2one('document.directory', 'Parent Item'), + 'parent_id': fields.many2one('document.directory', 'Parent Directory', select=1), 'child_ids': fields.one2many('document.directory', 'parent_id', 'Children'), 'file_ids': fields.one2many('ir.attachment', 'parent_id', 'Files'), 'content_ids': fields.one2many('document.directory.content', 'directory_id', 'Virtual Files'), - 'type': fields.selection([('directory','Static Directory'),('ressource','Other Resources')], 'Type', required=True), - 'ressource_type_id': fields.many2one('ir.model', 'Directories Mapped to Objects', - help="Select an object here and Open ERP will create a mapping for each of these " \ - "objects, using the given domain, when browsing through FTP."), + 'type': fields.selection([ + ('directory','Static Directory'), + ('ressource','Folders per resource'), + ], + 'Type', required=True, select=1, + help="Defines directory's behaviour."), + + 'ressource_type_id': fields.many2one('ir.model', 'Resource model', + help="Select an object here and there will be one folder per record of that resource."), 'resource_field': fields.many2one('ir.model.fields', 'Name field', help='Field to be used as name on resource directories. If empty, the "name" will be used.'), + 'resource_find_all': fields.boolean('Find all resources', required=True, + help="If true, all attachments that match this resource will " \ + " be located. If false, only ones that have this as parent." ), 'ressource_parent_type_id': fields.many2one('ir.model', 'Parent Model', help="If you put an object here, this directory template will appear bellow all of these objects. " \ "Don't put a parent directory if you select a parent model."), @@ -60,7 +67,7 @@ class document_directory(osv.osv): 'ressource_tree': fields.boolean('Tree Structure', help="Check this if you want to use the same tree structure as the object selected in the system."), 'dctx_ids': fields.one2many('document.directory.dctx', 'dir_id', 'Context fields'), - 'company_id': fields.many2one('res.company', 'Company'), + 'company_id': fields.many2one('res.company', 'Company'), } @@ -99,10 +106,12 @@ class document_directory(osv.osv): 'type': lambda *args: 'directory', 'ressource_id': lambda *a: 0, 'storage_id': _get_def_storage, + 'resource_find_all': True, } _sql_constraints = [ ('dirname_uniq', 'unique (name,parent_id,ressource_id,ressource_parent_type_id)', 'The directory name must be unique !'), - ('no_selfparent', 'check(parent_id <> id)', 'Directory cannot be parent of itself!') + ('no_selfparent', 'check(parent_id <> id)', 'Directory cannot be parent of itself!'), + ('dir_parented', 'check(parent_id IS NOT NULL OR storage_id IS NOT NULL)', 'Directory must have a parent or a storage') ] def name_get(self, cr, uid, ids, context={}): res = [] @@ -190,18 +199,25 @@ class document_directory(osv.osv): """ if not context: context = {} - lang = context.get('lang',False) - if not lang: - user = self.pool.get('res.users').browse(cr, uid, uid) - lang = user.context_lang - context['lang'] = lang - try: #just instrumentation - return nodes.get_node_context(cr, uid, context).get_uri(cr, uri) - except Exception,e: - print "exception: ",e - raise + return nodes.get_node_context(cr, uid, context).get_uri(cr, uri) + def get_dir_permissions(self, cr, uid, ids ): + """Check what permission user 'uid' has on directory 'id' + """ + assert len(ids) == 1 + id = ids[0] + + cr.execute( "SELECT count(dg.item_id) AS needs, count(ug.uid) AS has " \ + " FROM document_directory_group_rel dg " \ + " LEFT OUTER JOIN res_groups_users_rel ug " \ + " ON (dg.group_id = ug.gid AND ug.uid = %s) " \ + " WHERE dg.item_id = %s ", (uid, id)) + needs, has = cr.fetchone() + if needs and not has: + return 1 # still allow to descend into. + else: + return 15 def _locate_child(self, cr, uid, root_id, uri,nparent, ncontext): """ try to locate the node in uri, @@ -274,6 +290,8 @@ class document_directory(osv.osv): raise osv.except_osv(_('ValidateError'), _('Directory name contains special characters!')) return super(document_directory,self).create(cr, uid, vals, context) + # TODO def unlink(... + document_directory() class document_directory_dctx(osv.osv): diff --git a/addons/document/document_storage.py b/addons/document/document_storage.py index 90be2bc47df..419d6c03e37 100644 --- a/addons/document/document_storage.py +++ b/addons/document/document_storage.py @@ -24,7 +24,10 @@ from osv import osv, fields import os import tools import base64 +import errno import logging +from StringIO import StringIO +import psycopg2 from tools.misc import ustr from tools.translate import _ @@ -33,7 +36,9 @@ from osv.orm import except_orm import random import string +import pooler import netsvc +import nodes from content_index import cntIndex DMS_ROOT_PATH = tools.config.get('document_path', os.path.join(tools.config.get('root_path'), 'filestore')) @@ -84,6 +89,222 @@ def create_directory(path): os.makedirs(path) return dir_name +class nodefd_file(nodes.node_descriptor): + """ A descriptor to a real file + + Inheriting directly from file doesn't work, since file exports + some read-only attributes (like 'name') that we don't like. + """ + def __init__(self, parent, path, mode): + nodes.node_descriptor.__init__(self, parent) + self.__file = open(path, mode) + if mode.endswith('b'): + mode = mode[:-1] + self.mode = mode + + for attr in ('closed', 'read', 'write', 'seek', 'tell'): + setattr(self,attr, getattr(self.__file, attr)) + + def close(self): + # TODO: locking in init, close() + fname = self.__file.name + self.__file.close() + + if self.mode in ('w', 'w+', 'r+'): + par = self._get_parent() + cr = pooler.get_db(par.context.dbname).cursor() + icont = '' + mime = '' + filename = par.path + if isinstance(filename, (tuple, list)): + filename = '/'.join(filename) + + try: + mime, icont = cntIndex.doIndex(None, filename=filename, + content_type=None, realfname=fname) + except Exception: + logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True) + pass + + try: + icont_u = ustr(icont) + except UnicodeError: + icont_u = '' + + try: + fsize = os.stat(fname).st_size + cr.execute("UPDATE ir_attachment " \ + " SET index_content = %s, file_type = %s, " \ + " file_size = %s " \ + " WHERE id = %s", + (icont_u, mime, fsize, par.file_id)) + par.content_length = fsize + par.content_type = mime + cr.commit() + cr.close() + except Exception: + logging.getLogger('document.storage').warning('Cannot save file indexed content:', exc_info=True) + + elif self.mode in ('a', 'a+' ): + try: + par = self._get_parent() + cr = pooler.get_db(par.context.dbname).cursor() + fsize = os.stat(fname).st_size + cr.execute("UPDATE ir_attachment SET file_size = %s " \ + " WHERE id = %s", + (fsize, par.file_id)) + par.content_length = fsize + cr.commit() + cr.close() + except Exception: + logging.getLogger('document.storage').warning('Cannot save file appended content:', exc_info=True) + + + +class nodefd_db(StringIO, nodes.node_descriptor): + """ A descriptor to db data + """ + def __init__(self, parent, ira_browse, mode): + nodes.node_descriptor.__init__(self, parent) + if mode.endswith('b'): + mode = mode[:-1] + + if mode in ('r', 'r+'): + cr = ira_browse._cr # reuse the cursor of the browse object, just now + cr.execute('SELECT db_datas FROM ir_attachment WHERE id = %s',(ira_browse.id,)) + data = cr.fetchone()[0] + 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.storage').error("Incorrect mode %s specified", mode) + raise IOError(errno.EINVAL, "Invalid file mode") + self.mode = mode + + def close(self): + # we now open a *separate* cursor, to update the data. + # FIXME: this may be improved, for concurrency handling + 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() + icont = '' + mime = '' + filename = par.path + if isinstance(filename, (tuple, list)): + filename = '/'.join(filename) + + try: + mime, icont = cntIndex.doIndex(data, filename=filename, + content_type=None, realfname=None) + except Exception: + logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True) + pass + + try: + icont_u = ustr(icont) + except UnicodeError: + icont_u = '' + + out = psycopg2.Binary(data) + cr.execute("UPDATE ir_attachment " \ + "SET db_datas = %s, file_size=%s, " \ + " index_content= %s, file_type=%s " \ + " WHERE id = %s", + (out, len(data), icont_u, mime, par.file_id)) + elif self.mode == 'a': + data = self.getvalue() + out = psycopg2.Binary(data) + cr.execute("UPDATE ir_attachment " \ + "SET db_datas = COALESCE(db_datas,'') || %s, " \ + " file_size = COALESCE(file_size, 0) + %s " \ + " WHERE id = %s", + (out, len(data), par.file_id)) + cr.commit() + except Exception, e: + logging.getLogger('document.storage').exception('Cannot update db file #%d for close:', par.file_id) + raise + finally: + cr.close() + StringIO.close(self) + +class nodefd_db64(StringIO, nodes.node_descriptor): + """ A descriptor to db data, base64 (the old way) + + It stores the data in base64 encoding at the db. Not optimal, but + the transparent compression of Postgres will save the day. + """ + def __init__(self, parent, ira_browse, mode): + nodes.node_descriptor.__init__(self, parent) + if mode.endswith('b'): + mode = mode[:-1] + + if mode in ('r', 'r+'): + StringIO.__init__(self, base64.decodestring(ira_browse.db_datas)) + 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.storage').error("Incorrect mode %s specified", mode) + raise IOError(errno.EINVAL, "Invalid file mode") + self.mode = mode + + def close(self): + # we now open a *separate* cursor, to update the data. + # FIXME: this may be improved, for concurrency handling + 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() + icont = '' + mime = '' + filename = par.path + if isinstance(filename, (tuple, list)): + filename = '/'.join(filename) + + try: + mime, icont = cntIndex.doIndex(data, filename=filename, + content_type=None, realfname=None) + except Exception: + logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True) + pass + + try: + icont_u = ustr(icont) + except UnicodeError: + icont_u = '' + + cr.execute('UPDATE ir_attachment SET db_datas = %s::bytea, file_size=%s, ' \ + 'index_content = %s, file_type = %s ' \ + 'WHERE id = %s', + (base64.encodestring(out), len(out), icont_u, mime, par.file_id)) + elif self.mode == 'a': + out = self.getvalue() + # Yes, we're obviously using the wrong representation for storing our + # data as base64-in-bytea + cr.execute("UPDATE ir_attachment " \ + "SET db_datas = encode( (COALESCE(decode(encode(db_datas,'escape'),'base64'),'') || decode(%s, 'base64')),'base64')::bytea , " \ + " file_size = COALESCE(file_size, 0) + %s " \ + " WHERE id = %s", + (base64.encodestring(out), len(out), par.file_id)) + cr.commit() + except Exception, e: + logging.getLogger('document.storage').exception('Cannot update db file #%d for close:', par.file_id) + raise + finally: + cr.close() + StringIO.close(self) class document_storage(osv.osv): """ The primary object for data storage. @@ -110,7 +331,7 @@ class document_storage(osv.osv): 'group_ids': fields.many2many('res.groups', 'document_storage_group_rel', 'item_id', 'group_id', 'Groups'), 'dir_ids': fields.one2many('document.directory', 'parent_id', 'Directories'), 'type': fields.selection([('db', 'Database'), ('filestore', 'Internal File storage'), - ('realstore', 'External file storage'), ('virtual', 'Virtual storage')], 'Type', required=True), + ('realstore','External file storage'),], 'Type', required=True), 'path': fields.char('Path', size=250, select=1, help="For file storage, the root path of the storage"), 'online': fields.boolean('Online', help="If not checked, media is currently offline and its contents not available", required=True), 'readonly': fields.boolean('Read Only', help="If set, media is for reading only"), @@ -133,6 +354,49 @@ class document_storage(osv.osv): ('path_uniq', 'UNIQUE(type,path)', "The storage path must be unique!") ] + def __get_random_fname(self, path): + flag = None + # This can be improved + if os.path.isdir(path): + for dirs in os.listdir(path): + if os.path.isdir(os.path.join(path, dirs)) and len(os.listdir(os.path.join(path, dirs))) < 4000: + flag = dirs + break + flag = flag or create_directory(path) + filename = random_name() + return os.path.join(flag, filename) + + def __prepare_realpath(self, cr, file_node, ira, store_path, do_create=True): + """ Cleanup path for realstore, create dirs if needed + + @param file_node the node + @param ira ir.attachment browse of the file_node + @param store_path the path of the parent storage object, list + @param do_create create the directories, if needed + + @return tuple(path "/var/filestore/real/dir/", npath ['dir','fname.ext'] ) + """ + file_node.fix_ppath(cr, ira) + npath = file_node.full_path() or [] + # npath may contain empty elements, for root directory etc. + npath = filter(lambda x: x is not None, npath) + + # if self._debug: + # self._doclog.debug('Npath: %s', npath) + for n in npath: + if n == '..': + raise ValueError("Invalid '..' element in path") + for ch in ('*', '|', "\\", '/', ':', '"', '<', '>', '?',): + if ch in n: + raise ValueError("Invalid char %s in path %s" %(ch, n)) + dpath = [store_path,] + dpath += npath[:-1] + path = os.path.join(*dpath) + if not os.path.isdir(path): + self._doclog.debug("Create dirs: %s", path) + os.makedirs(path) + return path, npath + def get_data(self, cr, uid, id, file_node, context=None, fil_obj=None): """ retrieve the contents of some file_node having storage_id = id optionally, fil_obj could point to the browse object of the file @@ -141,15 +405,71 @@ class document_storage(osv.osv): if not context: context = {} boo = self.browse(cr, uid, id, context) + if not boo.online: + raise IOError(errno.EREMOTE, 'medium offline') + if fil_obj: ira = fil_obj else: ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context) return self.__get_data_3(cr, uid, boo, ira, context) - def __get_data_3(self, cr, uid, boo, ira, context): + def get_file(self, cr, uid, id, file_node, mode, context=None): + """ Return a file-like object for the contents of some node + """ + if context is None: + context = {} + boo = self.browse(cr, uid, id, context) if not boo.online: - raise RuntimeError('media offline') + raise IOError(errno.EREMOTE, 'medium offline') + + if boo.readonly and mode not in ('r', 'rb'): + raise IOError(errno.EPERM, "Readonly medium") + + ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context) + if boo.type == 'filestore': + if not ira.store_fname: + # On a migrated db, some files may have the wrong storage type + # try to fix their directory. + if mode in ('r','r+'): + if ira.file_size: + self._doclog.warning( "ir.attachment #%d does not have a filename, but is at filestore, fix it!" % ira.id) + raise IOError(errno.ENOENT, 'No file can be located') + else: + store_fname = self.__get_random_fname(boo.path) + cr.execute('UPDATE ir_attachment SET store_fname = %s WHERE id = %s', + (store_fname, ira.id)) + fpath = os.path.join(boo.path, store_fname) + else: + fpath = os.path.join(boo.path, ira.store_fname) + return nodefd_file(file_node, path=fpath, mode=mode) + + elif boo.type == 'db': + # TODO: we need a better api for large files + return nodefd_db(file_node, ira_browse=ira, mode=mode) + + elif boo.type == 'db64': + return nodefd_db64(file_node, ira_browse=ira, mode=mode) + + elif boo.type == 'realstore': + path, npath = self.__prepare_realpath(cr, file_node, ira, boo.path, + do_create = (mode[1] in ('w','a')) ) + fpath = os.path.join(path, npath[-1]) + if (not os.path.exists(fpath)) and mode[1] == 'r': + raise IOError("File not found: %s" % fpath) + elif mode[1] in ('w', 'a') and not ira.store_fname: + store_fname = os.path.join(*npath) + cr.execute('UPDATE ir_attachment SET store_fname = %s WHERE id = %s', + (store_fname, ira.id)) + return nodefd_file(file_node, path=fpath, mode=mode) + + elif boo.type == 'virtual': + raise ValueError('Virtual storage does not support static files') + + else: + raise TypeError("No %s storage" % boo.type) + + def __get_data_3(self, cr, uid, boo, ira, context): if boo.type == 'filestore': if not ira.store_fname: # On a migrated db, some files may have the wrong storage type @@ -159,13 +479,21 @@ class document_storage(osv.osv): return None fpath = os.path.join(boo.path, ira.store_fname) return file(fpath, 'rb').read() - elif boo.type == 'db': + elif boo.type == 'db64': # TODO: we need a better api for large files if ira.db_datas: out = base64.decodestring(ira.db_datas) else: out = '' return out + elif boo.type == 'db': + # We do an explicit query, to avoid type transformations. + cr.execute('SELECT db_datas FROM ir_attachment WHERE id = %s', (ira.id,)) + res = cr.fetchone() + if res: + return res[0] + else: + return '' elif boo.type == 'realstore': if not ira.store_fname: # On a migrated db, some files may have the wrong storage type @@ -179,7 +507,11 @@ class document_storage(osv.osv): elif not ira.store_fname: return None else: - raise IOError("File not found: %s" % fpath) + raise IOError(errno.ENOENT, "File not found: %s" % fpath) + + elif boo.type == 'virtual': + raise ValueError('Virtual storage does not support static files') + else: raise TypeError("No %s storage" % boo.type) @@ -197,30 +529,25 @@ class document_storage(osv.osv): ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context) if not boo.online: - raise RuntimeError('media offline') + raise IOError(errno.EREMOTE, 'medium offline') + + if boo.readonly: + raise IOError(errno.EPERM, "Readonly medium") + self._doclog.debug( "Store data for ir.attachment #%d" % ira.id) store_fname = None fname = None if boo.type == 'filestore': path = boo.path try: - flag = None - # This can be improved - if os.path.isdir(path): - for dirs in os.listdir(path): - if os.path.isdir(os.path.join(path, dirs)) and len(os.listdir(os.path.join(path, dirs))) < 4000: - flag = dirs - break - flag = flag or create_directory(path) - filename = random_name() - fname = os.path.join(path, flag, filename) + store_fname = self.__get_random_fname(path) + fname = os.path.join(path, store_fname) fp = file(fname, 'wb') fp.write(data) fp.close() self._doclog.debug( "Saved data to %s" % fname) filesize = len(data) # os.stat(fname).st_size - store_fname = os.path.join(flag, filename) - + # TODO Here, an old file would be left hanging. except Exception, e: @@ -228,27 +555,19 @@ class document_storage(osv.osv): raise except_orm(_('Error!'), str(e)) elif boo.type == 'db': filesize = len(data) - # will that work for huge data? TODO + # will that work for huge data? + out = psycopg2.Binary(data) + cr.execute('UPDATE ir_attachment SET db_datas = %s WHERE id = %s', + (out, file_node.file_id)) + elif boo.type == 'db64': + filesize = len(data) + # will that work for huge data? out = base64.encodestring(data) cr.execute('UPDATE ir_attachment SET db_datas = %s WHERE id = %s', (out, file_node.file_id)) elif boo.type == 'realstore': try: - file_node.fix_ppath(cr, ira) - npath = file_node.full_path() or [] - # npath may contain empty elements, for root directory etc. - for i, n in enumerate(npath): - if n == None: - del npath[i] - for n in npath: - for ch in ('*', '|', "\\", '/', ':', '"', '<', '>', '?', '..'): - if ch in n: - raise ValueError("Invalid char %s in path %s" %(ch, n)) - dpath = [boo.path,] - dpath += npath[:-1] - path = os.path.join(*dpath) - if not os.path.isdir(path): - os.makedirs(path) + path, npath = self.__prepare_realpath(cr, file_node, ira, boo.path, do_create=True) fname = os.path.join(path, npath[-1]) fp = file(fname,'wb') fp.write(data) @@ -260,6 +579,10 @@ class document_storage(osv.osv): except Exception,e : self._doclog.warning("Couldn't save data:", exc_info=True) raise except_orm(_('Error!'), str(e)) + + elif boo.type == 'virtual': + raise ValueError('Virtual storage does not support static files') + else: raise TypeError("No %s storage" % boo.type) @@ -276,16 +599,21 @@ class document_storage(osv.osv): self._doclog.debug('Cannot index file:', exc_info=True) pass + try: + icont_u = ustr(icont) + except UnicodeError: + icont_u = '' + # a hack: /assume/ that the calling write operation will not try # to write the fname and size, and update them in the db concurrently. # We cannot use a write() here, because we are already in one. cr.execute('UPDATE ir_attachment SET store_fname = %s, file_size = %s, index_content = %s, file_type = %s WHERE id = %s', - (store_fname, filesize, ustr(icont), mime, file_node.file_id)) + (store_fname, filesize, icont_u, mime, file_node.file_id)) file_node.content_length = filesize file_node.content_type = mime return True except Exception, e : - self._doclog.warning( "Couldn't save data:", exc_info=True) + self._doclog.warning("Couldn't save data:", exc_info=True) # should we really rollback once we have written the actual data? # at the db case (only), that rollback would be safe raise except_orm(_('Error at doc write!'), str(e)) @@ -295,7 +623,10 @@ class document_storage(osv.osv): files that have to be removed, too. """ if not storage_bo.online: - raise RuntimeError('media offline') + raise IOError(errno.EREMOTE, 'medium offline') + + if storage_bo.readonly: + raise IOError(errno.EPERM, "Readonly medium") if storage_bo.type == 'filestore': fname = fil_bo.store_fname @@ -303,7 +634,7 @@ class document_storage(osv.osv): return None path = storage_bo.path return (storage_bo.id, 'file', os.path.join(path, fname)) - elif storage_bo.type == 'db': + elif storage_bo.type in ('db', 'db64'): return None elif storage_bo.type == 'realstore': fname = fil_bo.store_fname @@ -312,7 +643,7 @@ class document_storage(osv.osv): path = storage_bo.path return ( storage_bo.id, 'file', os.path.join(path, fname)) else: - raise TypeError("No %s storage" % boo.type) + raise TypeError("No %s storage" % storage_bo.type) def do_unlink(self, cr, uid, unres): for id, ktype, fname in unres: @@ -326,6 +657,102 @@ class document_storage(osv.osv): return True + def simple_rename(self, cr, uid, file_node, new_name, context=None): + """ A preparation for a file rename. + It will not affect the database, but merely check and perhaps + rename the realstore file. + + @return the dict of values that can safely be be stored in the db. + """ + sbro = self.browse(cr, uid, file_node.storage_id, context=context) + assert sbro, "The file #%d didn't provide storage" % file_node.file_id + + if not sbro.online: + raise IOError(errno.EREMOTE, 'medium offline') + + if sbro.readonly: + raise IOError(errno.EPERM, "Readonly medium") + + if sbro.type in ('filestore', 'db', 'db64'): + # nothing to do for a rename, allow to change the db field + return { 'name': new_name, 'datas_fname': new_name } + elif sbro.type == 'realstore': + ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context) + + path, npath = self.__prepare_realpath(cr, file_node, ira, sbro.path, do_create=False) + fname = ira.store_fname + + if not fname: + self._doclog.warning("Trying to rename a non-stored file") + if fname != os.path.join(*npath): + self._doclog.warning("inconsistency in realstore: %s != %s" , fname, repr(npath)) + + oldpath = os.path.join(path, npath[-1]) + newpath = os.path.join(path, new_name) + os.rename(oldpath, newpath) + store_path = npath[:-1] + store_path.append(new_name) + store_fname = os.path.join(*store_path) + return { 'name': new_name, 'datas_fname': new_name, 'store_fname': store_fname } + else: + raise TypeError("No %s storage" % boo.type) + + def simple_move(self, cr, uid, file_node, ndir_bro, context=None): + """ A preparation for a file move. + It will not affect the database, but merely check and perhaps + move the realstore file. + + @param ndir_bro a browse object of document.directory, where this + file should move to. + @return the dict of values that can safely be be stored in the db. + """ + sbro = self.browse(cr, uid, file_node.storage_id, context=context) + assert sbro, "The file #%d didn't provide storage" % file_node.file_id + + if not sbro.online: + raise IOError(errno.EREMOTE, 'medium offline') + + if sbro.readonly: + raise IOError(errno.EPERM, "Readonly medium") + + par = ndir_bro + psto = None + while par: + if par.storage_id: + psto = par.storage_id.id + break + par = par.parent_id + if file_node.storage_id != psto: + self._doclog.debug('Cannot move file %r from %r to %r', file_node, file_node.parent, ndir_bro.name) + raise NotImplementedError('Cannot move files between storage media') + + if sbro.type in ('filestore', 'db', 'db64'): + # nothing to do for a rename, allow to change the db field + return { 'parent_id': ndir_bro.id } + elif sbro.type == 'realstore': + raise NotImplementedError("Cannot move in realstore, yet") # TODO + fname = fil_bo.store_fname + if not fname: + return ValueError("Tried to rename a non-stored file") + path = storage_bo.path + oldpath = os.path.join(path, fname) + + for ch in ('*', '|', "\\", '/', ':', '"', '<', '>', '?', '..'): + if ch in new_name: + raise ValueError("Invalid char %s in name %s" %(ch, new_name)) + + file_node.fix_ppath(cr, ira) + npath = file_node.full_path() or [] + dpath = [path,] + dpath.extend(npath[:-1]) + dpath.append(new_name) + newpath = os.path.join(*dpath) + # print "old, new paths:", oldpath, newpath + os.rename(oldpath, newpath) + return { 'name': new_name, 'datas_fname': new_name, 'store_fname': new_name } + else: + raise TypeError("No %s storage" % boo.type) + document_storage() diff --git a/addons/document/document_view.xml b/addons/document/document_view.xml index dec99fab65b..55b5a819104 100644 --- a/addons/document/document_view.xml +++ b/addons/document/document_view.xml @@ -16,7 +16,7 @@ <field name="online"/> <field name="readonly"/> </group> - <group colspan="2" attrs="{'invisible':[('type','=','db')]}"> + <group colspan="2" attrs="{'invisible':[('type','in',['db', 'db64'])]}"> <field name="path"/> </group> </form> @@ -61,6 +61,7 @@ name="Storage Media" action="action_document_storage_form" id="menu_document_storage_media" + groups="base.group_extended" parent="menu_document_management_configuration"/> <record model="ir.ui.view" id="view_document_directory_form"> @@ -68,27 +69,35 @@ <field name="model">document.directory</field> <field name="type">form</field> <field name="arch" type="xml"> - <form string="Directories"> + <form string="Directories" col="6"> <field name="name" select="1" colspan="4"/> - <field name="user_id"/> <field name="parent_id"/> - <field name="storage_id" /> + <field name="storage_id" widget="selection" /> + <field name="user_id"/> <field name="company_id" groups="base.group_multi_company" widget="selection"/> <notebook colspan="4"> <page string="Definition"> <separator string="Directory Type" colspan="4"/> <field name="type"/> - <field name="ressource_type_id" on_change="onchange_content_id(ressource_type_id)" attrs="{'required': [('type','=','ressource')], 'readonly': [('type','=','static')]}"/> - <newline/> - <field name="domain" attrs="{'required': [('type','=','ressource')], 'readonly': [('type','=','static')]}"/> - <field name="ressource_tree"/> - <field name="resource_field" domain="[('model_id','=',ressource_type_id), ('ttype', 'in', ('char', 'selection', 'date', 'datetime'))]"/> - <field name="ressource_id" select="2" readonly="1"/> - <field name="ressource_parent_type_id"/> + <group colspan="4" col="4" attrs="{'invisible': [('type','!=','ressource')]}"> + <field name="ressource_type_id" on_change="onchange_content_id(ressource_type_id)" + attrs="{'required': [('type','=','ressource')] }"/> + <field name="resource_find_all" groups="base.group_extended" /> + <newline/> + <field name="resource_field" domain="[('model_id','=',ressource_type_id), ('ttype', 'in', ('char', 'selection', 'date', 'datetime'))]"/> + <field name="ressource_tree"/> + <newline/> + <field name="domain" attrs="{'required': [('type','=','ressource')], 'readonly': [('type','=','static')]}"/> + </group> + <group colspan="4" col="4"> + <field name="ressource_parent_type_id"/> + <field name="ressource_id" select="2" readonly="1"/> + </group> + </page> - <page string="Generated Files"> + <page string="Generated Files" groups="base.group_extended"> <label colspan="4" string="For each entry here, virtual files will appear in this folder." /> - <field name="content_ids" nolabel="1" colspan="4" attrs="{'readonly': [('ressource_type_id','=',False)]}"> + <field name="content_ids" nolabel="1" colspan="4" > <form string="Contents"> <field name="name"/> <field name="sequence"/> @@ -108,6 +117,7 @@ </field> </page> <page string="Dynamic context" groups="base.group_extended"> + <label colspan="4" string="Define words in the context, for all child directories and files" /> <field name="dctx_ids" nolabel="1" colspan="4"> <tree string="Fields" editable="bottom"> <field name="field"/> @@ -120,6 +130,8 @@ </field> </page> <page string="Security"> + <label colspan="4" string="Only members of these groups will have access to this directory and its files." /> + <label colspan="4" string="These groups, however, do NOT apply to children directories, which must define their own groups." /> <field name="group_ids" colspan="4" nolabel="1"/> </page> </notebook> @@ -213,44 +225,46 @@ <field name="arch" type="xml"> <form string="Documents"> <group colspan="4" col="6"> - <field name="name" select="1" /> - <field name="type"/> + <field name="name" select="1" colspan="4" /> <field name="parent_id"/> + <newline/> + <field name="user_id"/> <field name="company_id" groups="base.group_multi_company" widget="selection"/> </group> <notebook colspan="4"> <page string="Attachment"> <group col="2" colspan="2"> <separator string="Data" colspan="2"/> + <field name="type"/> + <newline /> <group col="2" colspan="2" attrs="{'invisible':[('type','=','url')]}"> - <field name="datas" filename="datas_fname"/> - <field name="datas_fname" select="1"/> + <field name="datas" filename="datas_fname"/> + <field name="datas_fname" select="1"/> </group> <group col="2" colspan="2" attrs="{'invisible':[('type','=','binary')]}"> - <field name="url" widget="url"/> + <field name="url" widget="url"/> </group> </group> <group col="2" colspan="2"> - <separator string="Relation" colspan="2"/> - <field name="res_name" readonly="1"/> - <field name="partner_id"/> - <field name="user_id"/> - </group> - <group col="4" colspan="4"> - <separator string="History" colspan="4"/> - <group col="4" colspan="4"> - <field name="create_uid"/> - <field name="create_date"/> - </group> - <group col="4" colspan="4"> - <field name="write_uid"/> - <field name="write_date"/> + <field name="res_id" invisible="True"/> + <group col="2" colspan="2" attrs="{'invisible': [('res_id','=',0)]}"> + <separator string="Attached To" colspan="2"/> + <field name="res_model" readonly="True" invisible="True"/> + <field name="res_name" readonly="1"/> </group> + <separator string="Related to" colspan="2"/> + <field name="partner_id"/> + </group> + <group col="2" colspan="2" groups="base.group_extended"> + <separator string="Created" colspan="2"/> + <field name="create_uid" readonly="1"/> + <field name="create_date" readonly="1"/> + </group> + <group col="2" colspan="2" groups="base.group_extended"> + <separator string="Modified" colspan="2"/> + <field name="write_uid" readonly="1"/> + <field name="write_date" readonly="1"/> </group> - - </page> - <page string="Security"> - <field name="group_ids" colspan="4" nolabel="1"/> </page> <page string="Indexed Content" groups="base.group_extended"> <field name="index_content" colspan="4" nolabel="1"/> @@ -269,11 +283,13 @@ <field name="type">search</field> <field name="arch" type="xml"> <search string="IR Attachment"> - <filter icon="terp-go-month" string="Recent" - help="less 1 month modified/created attachments" + <filter icon="terp-go-month" string="Recent Month" + help="Attachment modified/created last 30 days" domain="[('create_date','<=', time.strftime('%%Y-%%m-%%d')),('create_date','>',(datetime.date.today()-datetime.timedelta(days=30)).strftime('%%Y-%%m-%%d'))]" /> <separator orientation="vertical"/> + <field name="name"/> + <field name="parent_id" /> <field name="user_id"> <filter icon="terp-personal" domain="[('user_id','=', False)]" @@ -284,9 +300,9 @@ <newline/> <group expand="0" string="Group By..."> <filter string="Partner" icon="terp-personal" domain="[]" - context="{'group_by':'partner_id'}" /> + context="{'group_by':'partner_id'}" groups="base.group_extended"/> <filter string="Directory" icon="terp-folder-green" domain="[]" context="{'group_by':'parent_id'}"/> - <filter string="Type" domain="[]" context="{'group_by':'type'}"/> + <filter string="Type" domain="[]" context="{'group_by':'type'}" groups="base.group_extended"/> <filter string="Owner" icon="terp-personal" domain="[]" context="{'group_by':'user_id'}"/> <filter string="Company" icon="terp-personal" domain="[]" context="{'group_by':'company_id'}" groups="base.group_multi_company"/> </group> @@ -301,11 +317,12 @@ <field name="arch" type="xml"> <tree colors="blue:type in ('url',)"> <field name="name"/> - <field name="type"/> - <field name="datas_fname"/> + <field name="parent_id" /> <field name="user_id"/> <field name="create_date"/> <field name="write_date"/> + <field name="partner_id" groups="base.group_extended" /> + <field name="type" groups="base.group_extended"/> </tree> </field> diff --git a/addons/document/nodes.py b/addons/document/nodes.py index dcd864e6db0..f1ac35c333c 100644 --- a/addons/document/nodes.py +++ b/addons/document/nodes.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################## -# +# # OpenERP, Open Source Management Solution # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). # @@ -15,21 +15,24 @@ # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. +# along with this program. If not, see <http://www.gnu.org/licenses/>. # ############################################################################## -import base64 -import StringIO +# 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 errno import os import time +from StringIO import StringIO + # # An object that represent an uri # path: the uri of the object @@ -49,7 +52,7 @@ class node_context(object): context """ cached_roots = {} - def __init__(self, cr, uid, context=None): + def __init__(self, cr, uid, context=None): self.dbname = cr.dbname self.uid = uid self.context = context @@ -57,26 +60,104 @@ class node_context(object): assert self._dirobj 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_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) - while duri: - ndir = ndir.child(cr, duri[0]) + 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: - return False - duri = duri[1:] + return False + 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 = 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) + + 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 node_file(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 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. + 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' + def __init__(self, path, parent, context): assert isinstance(context,node_context) assert (not parent ) or isinstance(parent,node_class) @@ -84,19 +165,29 @@ 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 = {} if parent: 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. + node. The returned list only contains the names of nodes. """ if self.parent: @@ -105,9 +196,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()" @@ -117,19 +213,43 @@ 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 - + 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") def get_etag(self,cr): """ 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) @@ -139,14 +259,14 @@ class node_class(object): wtime = time.mktime(time.strptime(self.write_date,'%Y-%m-%d %H:%M:%S')) else: wtime = time.time() return str(wtime) - + 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()") - + def get_dav_props(self, cr): """ If this class has special behaviour for GroupDAV etc, export its capabilities """ @@ -161,28 +281,96 @@ class node_class(object): def get_dav_eprop(self, cr, ns, prop): return None + 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 + """ + raise NotImplementedError(repr(self)) + + def create_child_collection(self, cr, objname): + """ Create a child collection (directory) under self + """ + raise NotImplementedError(repr(self)) + 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): 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 - + """ 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) + res = self._child_get(cr, domain=domain) + self._file_get(cr) return res - def child(self, cr, name, domain=None): + def child(self, cr, name, domain=None): res = self._child_get(cr, name, domain=None) if res: return res[0] @@ -196,48 +384,54 @@ class node_database(node_class): uid = self.context.uid ctx = self.context.context.copy() ctx.update(self.dctx) - where = [('parent_id','=',parent_id)] + where = [('parent_id','=',parent_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 + [('type', '=', 'directory')] ids = dirobj.search(cr, uid, where2, context=ctx) - res = [] - for dirr in dirobj.browse(cr, uid, ids, 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): + 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)) - - fil_obj = dirobj.pool.get('ir.attachment') + + 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 + class node_dir(node_database): our_type = 'collection' @@ -253,6 +447,10 @@ 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' + self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup') + self.uidperms = dirr.get_dir_permissions() if dctx: self.dctx.update(dctx) dc2 = self.context.context @@ -268,29 +466,55 @@ class node_dir(node_database): print e pass - - def get_data(self, cr): - res = '' - for child in self.children(cr): - res += child.get_data(cr) - return res + 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 None def _file_get(self, cr, nodename=False): - return super(node_dir,self)._file_get(cr, nodename, self.dir_id) + 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) def rmcol(self, cr): uid = self.context.uid - directory = self.context._dirobj.browse(cr, uid, self.dir_id) - res = False + directory = self.context._dirobj.browse(cr, uid, self.dir_id) + res = False if not directory: - raise OSError(2, 'Not such file or 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.') @@ -300,12 +524,15 @@ class node_dir(node_database): return res def create_child_collection(self, cr, objname): - object2 = False + 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() ctx.update(self.dctx) - obj = dirobj.browse(cr, uid, self.dir_id) + obj = dirobj.browse(cr, uid, self.dir_id) if obj and (obj.type == 'ressource') and not object2: raise OSError(1, 'Operation not permited.') @@ -315,15 +542,18 @@ class node_dir(node_database): 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False, 'ressource_id': object2 and object2.id or False, '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 """ + 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() @@ -354,11 +584,60 @@ class node_dir(node_database): 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(wtime) + def _get_ttag(self,cr): return 'dir-%d' % self.dir_id + 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") + + if (not self.check_perms('u')) or (not ndir_node.check_perms('w')): + raise IOError(errno.EPERM,"Permission denied") + + 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.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 created folders foreach resource in the foreign model. @@ -376,26 +655,42 @@ 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' + self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup') + self.uidperms = dirr.get_dir_permissions() 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! + # Important: the domain is evaluated using the *parent* dctx! self.domain = dirr.domain self.ressource_tree = dirr.ressource_tree # and then, we add our own vars in the dctx: if dctx: self.dctx.update(dctx) - + # and then, we prepare a dctx dict, for deferred evaluation: self.dctx_dict = {} 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) - 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] @@ -404,7 +699,7 @@ class node_res_dir(node_class): def _child_get(self, cr, name = None, domain=None): """ return virtual children of resource, based on the foreign object. - + Note that many objects use NULL for a name, so we should better call the name_search(),name_get() set of methods """ @@ -417,31 +712,45 @@ 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, self.dctx) + 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)) - + 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: object2 = False if self.resm_id: - object2 = dirobj.pool.get(self.res_model).browse(cr, uid, self.resm_id) or False - if obj._parent_name in obj.fields_get(cr, uid): + object2 = dirobj.pool.get(self.res_model).browse(cr, uid, self.resm_id) or False + 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) - res = [] + res = [] for bo in obj.browse(cr, uid, resids, context=ctx): if not bo: continue name = getattr(bo, self.namefield) if not name: continue - # Yes! we can't do better but skip nameless records. - + # Yes! we can't do better but skip nameless records. + res.append(node_res_obj(name, self.dir_id, self, self.context, self.res_model, bo)) return res @@ -458,7 +767,7 @@ class node_res_obj(node_class): def __init__(self, path, dir_id, parent, context, res_model, res_bo, res_id = None): super(node_res_obj,self).__init__(path, parent,context) assert parent - #todo: more info from dirr + #todo: more info from dirr self.dir_id = dir_id self.mimetype = 'application/x-directory' # 'httpd/unix-directory' @@ -466,17 +775,22 @@ 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.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 res_bo: - self.res_id = res_bo.id - dc2 = self.context.context + self.displayname = path + self.dctx_dict = parent.dctx_dict + self.res_find_all = parent.res_find_all + if res_bo: + self.res_id = res_bo.id + dc2 = self.context.context.copy() dc2.update(self.dctx) dc2['res_model'] = res_model dc2['res_id'] = res_bo.id - dc2['this'] = res_bo + dc2['this'] = res_bo for fld,expr in self.dctx_dict.items(): try: self.dctx[fld] = safe_eval(expr, dc2) @@ -487,11 +801,28 @@ class node_res_obj(node_class): else: self.res_id = res_id - def children(self, cr, domain=None): + 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, domain=None): - res = self._child_get(cr, name, domain=domain) + 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) @@ -501,6 +832,10 @@ class node_res_obj(node_class): 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() @@ -546,98 +881,113 @@ class node_res_obj(node_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) directory = dirobj.browse(cr, uid, self.dir_id) obj = dirobj.pool.get(self.res_model) where = [] - res = [] + res = [] if name: - where.append(('name','=',name)) - + where.append(('name','=',name)) + # Directory Structure display in tree structure - if self.res_id and directory.ressource_tree: - where1 = [] - if obj._parent_name in obj.fields_get(cr, uid): + if self.res_id and directory.ressource_tree: + where1 = [] + 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) + 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)) - - - where2 = where + [('parent_id','=',self.dir_id) ] - ids = dirobj.search(cr, uid, where2, context=ctx) + continue + res.append(node_res_obj(res_name, self.dir_id, self, self.context, self.res_model, res_bo = bo)) + + + where2 = where + [('parent_id','=',self.dir_id) ] + ids = dirobj.search(cr, uid, where2, context=ctx) for dirr in dirobj.browse(cr, uid, ids, context=ctx): 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)) 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})) - - - + + + 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 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)) - - - # 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)] + + + # 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)] - dirids = dirobj.search(cr,uid, where5) + dirids = dirobj.search(cr,uid, where5) where5 = where4 + [('ressource_id','=',self.res_id)] - dirids = dirids + dirobj.search(cr,uid, where5) + 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)) if dirr.type == 'ressource': - res.append(node_res_dir(dirr.name, self, self.context, dirr, {'active_id': self.res_id})) + res.append(node_res_dir(dirr.name, self, self.context, dirr, {'active_id': self.res_id})) 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']) - - object2 = res_obj.browse(cr, uid, self.context.context['res_id']) or False - - obj = dirobj.browse(cr, uid, self.dir_id) + res_obj = dirobj.pool.get(self.context.context['res_model']) + + object2 = res_obj.browse(cr, uid, self.context.context['res_id']) or False + + obj = dirobj.browse(cr, uid, self.dir_id) if obj and (obj.type == 'ressource') and not object2: raise OSError(1, 'Operation not permited.') - + val = { 'name': objname, 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False, 'ressource_id': object2 and object2.id or False, 'parent_id' : False - } - if (obj and (obj.type in ('directory'))) or not object2: - val['parent_id'] = obj and obj.id or False - + } + 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() @@ -646,16 +996,18 @@ 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 } + 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) fnode = node_file(path, self, self.context, fil) - fnode.set_data(cr, data, fil) + if data is not None: + fnode.set_data(cr, data, fil) return fnode def _get_ttag(self,cr): @@ -674,40 +1026,64 @@ class node_file(node_class): 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 + + self.uuser = (fil.user_id and fil.user_id.login) or '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 + 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 + 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") - def open(self, cr, mode=False): - uid = self.context.uid - if self.type in ('collection','database'): - 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 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 - document = document_obj.browse(cr, uid, self.file_id, context=self.context.context) + return False + document = document_obj.browse(cr, uid, self.file_id, context=self.context.context) res = False if document and document._table_name == 'ir.attachment': res = document_obj.unlink(cr, uid, [document.id]) - return res + return res def fix_ppath(self, cr, fbro): """Sometimes we may init this w/o path, parent. This function fills the missing path from the file browse object - + Note: this may be an expensive operation, do on demand. However, once caching is in, we might want to do that at init time and keep this object anyway @@ -716,7 +1092,7 @@ class node_file(node_class): return assert fbro uid = self.context.uid - + dirpath = [] if fbro.parent_id: dirobj = self.context._dirobj.pool.get('document.directory') @@ -725,25 +1101,25 @@ class node_file(node_class): dirpath.append(fbro.datas_fname) else: dirpath.append(fbro.name) - + if len(dirpath)>1: self.path = dirpath else: - self.path = dirpath[0] - + self.path = dirpath[0] + def get_data(self, cr, fil_obj = None): - """ Retrieve the data for some file. + """ Retrieve the data for some file. fil_obj may optionally be specified, and should be a browse object for the file. This is useful when the caller has already initiated 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) @@ -755,24 +1131,80 @@ class node_file(node_class): return self.content_length def set_data(self, cr, data, fil_obj = None): - """ Store data at some file. + """ Store data at some file. fil_obj may optionally be specified, and should be a browse object for the file. This is useful when the caller has already initiated 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) 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.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.id) + assert self.parent + + ret = {} + if 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): @@ -781,6 +1213,12 @@ class node_content(node_class): 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. @@ -789,23 +1227,13 @@ 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): """ Try to read the object and fill missing fields, like mimetype, dates etc. This function must be different from the constructor, because it uses the db cursor. """ - + cr.execute('SELECT DISTINCT mimetype FROM document_directory_content_type WHERE active AND code = %s;', (self.extension,)) res = cr.fetchall() @@ -815,6 +1243,9 @@ class node_content(node_class): 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) @@ -822,16 +1253,91 @@ class node_content(node_class): 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) def _get_ttag(self,cr): return 'cnt-%d%s' % (self.cnt_id,(self.act_id and ('-' + str(self.act_id))) or '') + + +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 + + 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: + 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 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() + cntobj = par.context._dirobj.pool.get('document.directory.content') + cntobj.process_write(cr, uid, parent, data, ctx) + elif self.mode == 'a': + raise NotImplementedError + cr.commit() + except Exception, e: + logging.getLogger('document.content').exception('Cannot update db content #%d for close:', par.cnt_id) + raise + finally: + cr.close() + StringIO.close(self) diff --git a/addons/document/report/document_report_view.xml b/addons/document/report/document_report_view.xml index 00743d768e7..d0ba1303a39 100644 --- a/addons/document/report/document_report_view.xml +++ b/addons/document/report/document_report_view.xml @@ -33,22 +33,22 @@ </field> </record> - <record id="view_report_document_user_search" model="ir.ui.view"> + <record id="view_report_document_user_search" model="ir.ui.view"> <field name="name">report.document.user.search</field> <field name="model">report.document.user</field> <field name="type">search</field> <field name="arch" type="xml"> <search string="All users files"> - <group col="12" colspan="4"> + <group col="12" colspan="4"> <separator orientation="vertical"/> - <filter icon="terp-go-year" string="This Year" domain="[('name','=',time.localtime()[0])]" help="All Months Files"/> - <filter icon="terp-go-month" string="This Month" domain="[('month','=',time.localtime()[1])]" help="This Months Files"/> - <separator orientation="vertical"/> - <field name="name" select="1"/> - <field name="month" select="1"/> - <field name="user" select="1"/> - <field name="directory" select="1"/> - </group> + <filter icon="terp-go-year" string="This Year" domain="[('name','=',time.localtime()[0])]" help="All Months Files"/> + <filter icon="terp-go-month" string="This Month" domain="[('month','=',time.localtime()[1])]" help="This Months Files"/> + <separator orientation="vertical"/> + <field name="name" select="1"/> + <field name="month" select="1"/> + <field name="user" select="1"/> + <field name="directory" select="1"/> + </group> </search> </field> </record> @@ -61,7 +61,7 @@ <field name="res_model">report.document.user</field> <field name="view_type">form</field> <field name="view_mode">tree</field> - <field name="context">{"search_default_user":uid}</field> + <field name="context">{'search_default_user': 'user_id'}</field> <field name="search_view_id" ref="view_report_document_user_search"/> </record> diff --git a/addons/document/test/document_test2.yml b/addons/document/test/document_test2.yml new file mode 100644 index 00000000000..712399afac3 --- /dev/null +++ b/addons/document/test/document_test2.yml @@ -0,0 +1,72 @@ +- + In order to test the document management +- + I make sure the default installation has some storage and root directory. +- + !assert {model: document.storage, id: storage_default }: + - id != False +- + !assert {model: document.directory, id: dir_root}: + - storage_id != False +- + I create a "Testing" folder where all the test data will go. +- + !record {model: document.directory, id: dir_tests }: + name: 'Testing' + parent_id: dir_root +- + I create an attachment into the root folder (w. empty fields, test that + defaults work) +- + !record {model: ir.attachment, id: file_test1 }: + name: Test file +- + I delete the attachment from the root folder +- + !delete {model: ir.attachment, id: file_test1, search: } +- + I create an attachment into the Testing folder. +- + !record {model: ir.attachment, id: file_test2 }: + name: Test file 2 + parent_id: dir_tests +- + I update the attachment with data, namely "abcd" +- + !record {model: ir.attachment, id: file_test2 }: + datas: "abcd" +- + I test that the datas of the attachment are correct +- + !assert {model: ir.attachment, id: file_test2 }: + - datas == "abcd\n" + - file_size == 5 + - file_type == 'text/plain' +- + I rename the attachment. +- + !record {model: ir.attachment, id: file_test2 }: + name: Test renamed 2 +- + I search the testing folder for attachments. +- + !python {model: ir.attachment}: | + ids = self.search(cr, uid, [('parent_id.name','=', 'Testing'), ('name','=','Test renamed 2')]) + assert ids == [ ref("file_test2") ], ids +- + I create an attachment to a 3rd resource, eg. a res.country +- + !record {model: ir.attachment, id: attach_3rd }: + name: 'Res country attachment.txt' + parent_id: dir_tests + datas: 'defg' + res_model: res.country + res_id: !eval ref("base.za") +- + I search for the res.country attachment +- + !python {model: ir.attachment}: | + ids = self.search(cr, uid, [('res_model', '=', 'res.country'), ('res_id', '=', ref("base.za"))]) + assert ids == [ ref("attach_3rd")], ids +- + !delete {model: ir.attachment, id: attach_3rd, search: } |
