[IMP] ir.attachment : move the fields index_content (from document), mimetype and datas_checksum (from website) to base. In base, the mimetype is computed with the Magic python lib (if installed, else with a custom function using magic number), and the index_content is an implementation of strings unix command. In document, index_content is still computed with the indexer of document module.

This commit is contained in:
Jérome Maes
2014-12-11 16:11:54 +01:00
parent 2583258f93
commit e06adc7e08
5 changed files with 142 additions and 87 deletions
+21 -23
View File
@@ -47,6 +47,15 @@ _logger = logging.getLogger(__name__)
class document_file(osv.osv):
_inherit = 'ir.attachment'
def _index(self, cr, uid, bin_data, datas_fname, mimetype):
"""
Override the parent method to use the indexer of document module to find the index_content.
It is called from _set_data, when the data changed.
"""
mime, icont = cntIndex.doIndex(bin_data, datas_fname, mimetype or None, None)
icont_u = ustr(icont)
return icont_u
_columns = {
# Columns from ir.attachment:
'write_date': fields.datetime('Date Modified', readonly=True),
@@ -54,9 +63,7 @@ class document_file(osv.osv):
# Fields of document:
'user_id': fields.many2one('res.users', 'Owner', select=1),
'parent_id': fields.many2one('document.directory', 'Directory', select=1, change_default=True),
'index_content': fields.text('Indexed Content'),
'partner_id':fields.many2one('res.partner', 'Partner', select=1),
'file_type': fields.char('Content Type'),
}
_order = "id desc"
@@ -74,7 +81,7 @@ class document_file(osv.osv):
ids = [ids]
super(document_file, self).check(cr, uid, ids, mode, context=context, values=values)
if ids:
self.pool.get('ir.model.access').check(cr, uid, 'document.directory', mode)
@@ -124,22 +131,13 @@ class document_file(osv.osv):
# take partner from uid
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)
if vals.get('datas', False):
vals['file_type'], vals['index_content'] = self._index(cr, uid, vals['datas'].decode('base64'), vals.get('datas_fname', False), None)
return super(document_file, self).create(cr, uid, vals, context)
def write(self, cr, uid, ids, vals, context=None):
if context is None:
context = {}
if vals.get('datas', False):
vals['file_type'], vals['index_content'] = self._index(cr, uid, vals['datas'].decode('base64'), vals.get('datas_fname', False), None)
return super(document_file, self).write(cr, uid, ids, vals, context)
def _index(self, cr, uid, data, datas_fname, file_type):
mime, icont = cntIndex.doIndex(data, datas_fname, file_type or None, None)
icont_u = ustr(icont)
return mime, icont_u
def __get_partner_id(self, cr, uid, res_model, res_id, context=None):
""" 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
@@ -395,7 +393,7 @@ 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',
'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'),
@@ -405,17 +403,17 @@ class document_directory_content(osv.osv):
'sequence': lambda *args: 1,
'include_name': lambda *args: 1,
}
def _file_get(self, cr, node, nodename, content, context=None):
""" return the nodes of a <node> parent having a <content> content
The return value MUST be false or a list of node_class objects.
"""
# TODO: respect the context!
model = node.res_model
if content.include_name and not model:
return False
res2 = []
tname = ''
if content.include_name:
@@ -447,7 +445,7 @@ class document_directory_content(osv.osv):
if node.extension != '.pdf':
raise Exception("Invalid content: %s" % node.extension)
return True
def process_read(self, cr, uid, node, context=None):
if node.extension != '.pdf':
raise Exception("Invalid content: %s" % node.extension)
@@ -546,11 +544,11 @@ class document_storage(osv.osv):
# 2nd phase: store the metadata
try:
icont = ''
mime = ira.file_type
mime = ira.mimetype
if not mime:
mime = ""
try:
mime, icont = cntIndex.doIndex(data, ira.datas_fname, ira.file_type or None, fname)
mime, icont = cntIndex.doIndex(data, ira.datas_fname, ira.mimetype or None, fname)
except Exception:
_logger.debug('Cannot index file.', exc_info=True)
pass
@@ -561,8 +559,8 @@ class document_storage(osv.osv):
# 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 file_size = %s, index_content = %s, file_type = %s WHERE id = %s', (filesize, icont_u, mime, file_node.file_id))
self.pool.get('ir.attachment').invalidate_cache(cr, uid, ['file_size', 'index_content', 'file_type'], [file_node.file_id], context=context)
cr.execute('UPDATE ir_attachment SET file_size = %s, index_content = %s, mimetype = %s WHERE id = %s', (filesize, icont_u, mime, file_node.file_id))
self.pool.get('ir.attachment').invalidate_cache(cr, uid, ['file_size', 'index_content', 'mimetype'], [file_node.file_id], context=context)
file_node.content_length = filesize
file_node.content_type = mime
return True
@@ -1650,8 +1648,8 @@ class node_file(node_class):
super(node_file,self).__init__(path, parent,context)
self.file_id = fil.id
#todo: more info from ir_attachment
if fil.file_type and '/' in fil.file_type:
self.mimetype = str(fil.file_type)
if fil.mimetype and '/' in fil.mimetype:
self.mimetype = str(fil.mimetype)
self.create_date = fil.create_date
self.write_date = fil.write_date or fil.create_date
self.content_length = fil.file_size
+2 -7
View File
@@ -179,15 +179,10 @@
<xpath expr="//div[@name='creation_div']" position='after'>
<label for="write_uid" string="Modification"/>
<div>
<field name="write_uid" readonly="1" class="oe_inline"/> on
<field name="write_uid" readonly="1" class="oe_inline"/> on
<field name="write_date" readonly="1" class="oe_inline"/>
</div>
</xpath>
<xpath expr="//group[@name='description_group']" position='after'>
<group string="Indexed Content" colspan="4">
<field name="index_content" nolabel="1"/>
</group>
</xpath>
</field>
</record>
@@ -246,7 +241,7 @@
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new document.
Click to create a new document.
</p><p>
The Documents repository gives you access to all attachments, such
as mails, project documents, invoices etc.
+3 -36
View File
@@ -698,8 +698,11 @@ class website_menu(osv.osv):
self.write(cr, uid, [menu['id']], menu, context=context)
return True
class ir_attachment(osv.osv):
_inherit = "ir.attachment"
def _website_url_get(self, cr, uid, ids, name, arg, context=None):
result = {}
for attach in self.browse(cr, uid, ids, context=context):
@@ -708,21 +711,6 @@ class ir_attachment(osv.osv):
else:
result[attach.id] = self.pool['website'].image_url(cr, uid, attach, 'datas')
return result
def _datas_checksum(self, cr, uid, ids, name, arg, context=None):
result = dict.fromkeys(ids, False)
attachments = self.read(cr, uid, ids, ['res_model'], context=context)
view_attachment_ids = [attachment['id'] for attachment in attachments if attachment['res_model'] == 'ir.ui.view']
for attach in self.read(cr, uid, view_attachment_ids, ['res_model', 'res_id', 'type', 'datas'], context=context):
result[attach['id']] = self._compute_checksum(attach)
return result
def _compute_checksum(self, attachment_dict):
if attachment_dict.get('res_model') == 'ir.ui.view'\
and not attachment_dict.get('res_id') and not attachment_dict.get('url')\
and attachment_dict.get('type', 'binary') == 'binary'\
and attachment_dict.get('datas'):
return hashlib.new('sha1', attachment_dict['datas']).hexdigest()
return None
def _datas_big(self, cr, uid, ids, name, arg, context=None):
result = dict.fromkeys(ids, False)
@@ -739,32 +727,11 @@ class ir_attachment(osv.osv):
return result
_columns = {
'datas_checksum': fields.function(_datas_checksum, size=40,
string="Datas checksum", type='char', store=True, select=True),
'website_url': fields.function(_website_url_get, string="Attachment URL", type='char'),
'datas_big': fields.function (_datas_big, type='binary', store=True,
string="Resized file content"),
'mimetype': fields.char('Mime Type', readonly=True),
}
def _add_mimetype_if_needed(self, values):
if values.get('datas_fname'):
values['mimetype'] = mimetypes.guess_type(values.get('datas_fname'))[0] or 'application/octet-stream'
def create(self, cr, uid, values, context=None):
chk = self._compute_checksum(values)
if chk:
match = self.search(cr, uid, [('datas_checksum', '=', chk)], context=context)
if match:
return match[0]
self._add_mimetype_if_needed(values)
return super(ir_attachment, self).create(
cr, uid, values, context=context)
def write(self, cr, uid, ids, values, context=None):
self._add_mimetype_if_needed(values)
return super(ir_attachment, self).write(cr, uid, ids, values, context=context)
def try_remove(self, cr, uid, ids, context=None):
""" Removes a web-based image attachment if it is used by no view
(template)
+110 -19
View File
@@ -32,20 +32,56 @@ from openerp.osv import fields,osv
from openerp import SUPERUSER_ID
from openerp.osv.orm import except_orm
from openerp.tools.translate import _
from openerp.tools.misc import ustr
_logger = logging.getLogger(__name__)
# optional python-magic import (https://github.com/ahupp/python-magic)
try:
import magic
except ImportError:
magic = None
def guess_mimetype(bin_data):
if magic:
# if binary not identified or error, will return 'application/octet-stream'
return magic.from_buffer(bin_data, mime=True)
else:
# else, guess the type using the magic number of file hex signature (like magic, but more limited)
# see http://www.filesignatures.net/ for file signatures
mapping = {
# pdf
'application/pdf' : ['%PDF'],
# jpg, jpeg, png, gif
'image/jpeg' : ['\xFF\xD8\xFF\xE0', '\xFF\xD8\xFF\xE2', '\xFF\xD8\xFF\xE3', '\xFF\xD8\xFF\xE1'],
'image/png' : ['\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'],
'image/gif' : ['GIF87a', '\x47\x49\x46\x38\x37\x61', 'GIF89a', '\x47\x49\x46\x38\x39\x61'],
# zip, but will include jar, odt, ods, odp, docx, xlsx, pptx, apk
'application/zip' : ['PK'],
# doc, xls
'application/msword' : ['\xCF\x11\xE0\xA1\xB1\x1A\xE1\x00', '\xEC\xA5\xC1\x00', '\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1', '\x0D\x44\x4F\x43'],
'application/vnd.ms-excel' : ['\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1','\x09\x08\x10\x00\x00\x06\x05\x00','\xFD\xFF\xFF\xFF\x10','\xFD\xFF\xFF\xFF\x1F','\xFD\xFF\xFF\xFF\x23','\xFD\xFF\xFF\xFF\x28','\xFD\xFF\xFF\xFF\x29'],
}
for mimetype in mapping.keys():
for signature in mapping[mimetype]:
if bin_data.startswith(signature):
return mimetype
return 'application/octet-stream'
class ir_attachment(osv.osv):
"""Attachments are used to link binary files or url to any openerp document.
External attachment storage
---------------------------
The 'data' function field (_data_get,data_set) is implemented using
_file_read, _file_write and _file_delete which can be overridden to
implement other storage engines, shuch methods should check for other
location pseudo uri (example: hdfs://hadoppserver)
The default implementation is the file:dirname location that stores files
on the local filesystem using name based on their sha1 hash
"""
@@ -62,7 +98,7 @@ class ir_attachment(osv.osv):
if res_name:
field = self._columns.get('res_name',False)
if field and len(res_name) > field.size:
res_name = res_name[:30] + '...'
res_name = res_name[:30] + '...'
data[attachment.id] = res_name or False
else:
data[attachment.id] = False
@@ -98,9 +134,7 @@ class ir_attachment(osv.osv):
path = path.strip('/\\')
return os.path.join(self._filestore(cr, uid), path)
def _get_path(self, cr, uid, bin_data):
sha = hashlib.sha1(bin_data).hexdigest()
def _get_path(self, cr, uid, bin_data, sha):
# retro compatibility
fname = sha[:3] + '/' + sha
full_path = self._full_path(cr, uid, fname)
@@ -128,9 +162,9 @@ class ir_attachment(osv.osv):
_logger.exception("_read_file reading %s", full_path)
return r
def _file_write(self, cr, uid, value):
def _file_write(self, cr, uid, value, checksum):
bin_value = value.decode('base64')
fname, full_path = self._get_path(cr, uid, bin_value)
fname, full_path = self._get_path(cr, uid, bin_value, checksum)
if not os.path.exists(full_path):
try:
with open(full_path, 'wb') as fp:
@@ -166,19 +200,39 @@ class ir_attachment(osv.osv):
def _data_set(self, cr, uid, id, name, value, arg, context=None):
# We dont handle setting data to null
if not value:
# reset computed fields
super(ir_attachment, self).write(cr, SUPERUSER_ID, [id], {'file_size' : 0, 'checksum' : None, 'mimetype' : None, 'index_content' : None}, context=context)
return True
if context is None:
context = {}
location = self._storage(cr, uid, context)
file_size = len(value.decode('base64'))
# browse the attachment and get the file to delete
attach = self.browse(cr, uid, id, context=context)
fname_to_delete = attach.store_fname
location = self._storage(cr, uid, context)
# compute the field depending of datas
bin_data = value.decode('base64')
mimetype = self._compute_mimetype(bin_data)
checksum = self._compute_checksum(bin_data)
vals = {
'file_size': len(bin_data),
'checksum' : checksum,
'mimetype' : mimetype,
'index_content' : self._index(cr, SUPERUSER_ID, bin_data, attach.datas_fname, mimetype),
}
if location != 'db':
fname = self._file_write(cr, uid, value)
# SUPERUSER_ID as probably don't have write access, trigger during create
super(ir_attachment, self).write(cr, SUPERUSER_ID, [id], {'store_fname': fname, 'file_size': file_size, 'db_datas': False}, context=context)
# create the file
fname = self._file_write(cr, uid, value, checksum)
vals.update({
'store_fname': fname,
'db_datas': False
})
else:
super(ir_attachment, self).write(cr, SUPERUSER_ID, [id], {'db_datas': value, 'file_size': file_size, 'store_fname': False}, context=context)
vals.update({
'store_fname': False,
'db_datas': value
})
# SUPERUSER_ID as probably don't have write access, trigger during create
super(ir_attachment, self).write(cr, SUPERUSER_ID, [id], vals, context=context)
# After de-referencing the file in the database, check whether we need
# to garbage-collect it on the filesystem
@@ -186,6 +240,36 @@ class ir_attachment(osv.osv):
self._file_delete(cr, uid, fname_to_delete)
return True
def _compute_checksum(self, bin_data):
""" compute the checksum for the given datas
:param bin_data : datas in its binary form
"""
if bin_data:
return hashlib.sha1(bin_data).hexdigest()
return False
def _compute_mimetype(self, bin_data):
""" compute the mimetype of the given datas
:param bin_data : binary data
:return mime : string indicating the mimetype, or application/octet-stream by default
"""
return guess_mimetype(bin_data)
def _index(self, cr, uid, bin_data, datas_fname, file_type):
""" compute the index content of the given filename, or binary data.
This is a python implementation of the unix command 'strings'.
:param bin_data : datas in binary form
:return index_content : string containing all the printable character of the binary data
"""
index_content = False
if file_type:
index_content = file_type.split('/')[0]
if index_content == 'text': # compute index_content only for text type
words = re.findall("[^\x00-\x1F\x7F-\xFF]{4,}", bin_data)
index_content = ustr("\n".join(words))
return index_content
_name = 'ir.attachment'
_columns = {
'name': fields.char('Attachment Name', required=True),
@@ -204,12 +288,17 @@ class ir_attachment(osv.osv):
'datas': fields.function(_data_get, fnct_inv=_data_set, string='File Content', type="binary", nodrop=True),
'store_fname': fields.char('Stored Filename'),
'db_datas': fields.binary('Database Data'),
'file_size': fields.integer('File Size'),
# computed fields depending on datas
'file_size': fields.integer('File Size', readonly=True),
'checksum': fields.char("Checksum/SHA1", size=40, select=True, readonly=True),
'mimetype': fields.char('Mime Type', readonly=True),
'index_content': fields.text('Indexed Content', readonly=True),
}
_defaults = {
'type': 'binary',
'file_size': 0,
'mimetype' : False,
'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'ir.attachment', context=c),
}
@@ -319,8 +408,9 @@ class ir_attachment(osv.osv):
if isinstance(ids, (int, long)):
ids = [ids]
self.check(cr, uid, ids, 'write', context=context, values=vals)
if 'file_size' in vals:
del vals['file_size']
# remove computed field depending of datas
for field in ['file_size', 'checksum', 'mimetype']:
vals.pop(field, False)
return super(ir_attachment, self).write(cr, uid, ids, vals, context)
def copy(self, cr, uid, id, default=None, context=None):
@@ -346,9 +436,10 @@ class ir_attachment(osv.osv):
return res
def create(self, cr, uid, values, context=None):
# remove computed field depending of datas
for field in ['file_size', 'checksum', 'mimetype']:
values.pop(field, False)
self.check(cr, uid, [], mode='write', context=context, values=values)
if 'file_size' in values:
del values['file_size']
return super(ir_attachment, self).create(cr, uid, values, context)
def action_get(self, cr, uid, context=None):
@@ -17,6 +17,7 @@
<field name="datas" filename="datas_fname" attrs="{'invisible':[('type','=','url')]}"/>
<field name="datas_fname" invisible="1" attrs="{'invisible':[('type','=','url')]}" class="oe_inline oe_right"/>
<field name="url" widget="url" attrs="{'invisible':[('type','=','binary')]}"/>
<field name="mimetype"/>
</group>
<group string="Attached To">
<field name="res_model"/>
@@ -27,13 +28,16 @@
<group groups="base.group_no_one" string="History">
<label for="create_uid" string="Creation"/>
<div name="creation_div">
<field name="create_uid" readonly="1" class="oe_inline"/> on
<field name="create_uid" readonly="1" class="oe_inline"/> on
<field name="create_date" readonly="1" class="oe_inline"/>
</div>
</group>
<group name="description_group" string="Description" colspan="4">
<field name="description" nolabel="1"/>
</group>
<group string="Indexed Content" colspan="4">
<field name="index_content" nolabel="1"/>
</group>
</group>
</sheet>
</form>
@@ -67,7 +71,7 @@
string="Binary"
domain="[('type','=','binary')]"/>
<separator/>
<filter name="my_documents_filter"
<filter name="my_documents_filter"
string="My Document(s)"
icon="terp-personal"
domain="[('create_uid','=',uid)]"