This commit is contained in:
vrenaville
2022-05-17 13:31:17 +02:00
parent 43ed7178af
commit 18d07353d9
@@ -20,21 +20,19 @@ _logger = logging.getLogger(__name__)
def clean_fs(files): def clean_fs(files):
_logger.info('cleaning old files from filestore') _logger.info("cleaning old files from filestore")
for full_path in files: for full_path in files:
if os.path.exists(full_path): if os.path.exists(full_path):
try: try:
os.unlink(full_path) os.unlink(full_path)
except OSError: except OSError:
_logger.info( _logger.info(
"_file_delete could not unlink %s", "_file_delete could not unlink %s", full_path, exc_info=True
full_path, exc_info=True
) )
except IOError: except IOError:
# Harmless and needed for race conditions # Harmless and needed for race conditions
_logger.info( _logger.info(
"_file_delete could not unlink %s", "_file_delete could not unlink %s", full_path, exc_info=True
full_path, exc_info=True
) )
@@ -52,34 +50,32 @@ def savepoint(cursor):
class IrAttachment(osv.osv): class IrAttachment(osv.osv):
_inherit = 'ir.attachment' _inherit = "ir.attachment"
@staticmethod @staticmethod
def _compute_checksum(bin_data): def _compute_checksum(bin_data):
""" compute the checksum for the given datas """compute the checksum for the given datas
:param bin_data : datas in its binary form :param bin_data : datas in its binary form
""" """
# an empty file has a checksum too (for caching) # an empty file has a checksum too (for caching)
return hashlib.sha1(bin_data or '').hexdigest() return hashlib.sha1(bin_data or "").hexdigest()
def _is_user_admin(self, cr, uid): def _is_user_admin(self, cr, uid):
if uid == SUPERUSER_ID: if uid == SUPERUSER_ID:
return True return True
else: else:
return self.pool.get('res.users').has_group( return self.pool.get("res.users").has_group(
cr, uid, 'base.group_erp_manager' cr, uid, "base.group_erp_manager"
) )
def _storage(self, cr, uid, context=None): def _storage(self, cr, uid, context=None):
return self.pool['ir.config_parameter'].get_param( return self.pool["ir.config_parameter"].get_param(
cr, SUPERUSER_ID, 'ir_attachment.location', 'file' cr, SUPERUSER_ID, "ir_attachment.location", "file"
) )
def _full_path(self, cr, uid, location, path): def _full_path(self, cr, uid, location, path):
# Hack to allow filestore migration from local filesystem to any remote # Hack to allow filestore migration from local filesystem to any remote
return super(IrAttachment, self)._full_path( return super(IrAttachment, self)._full_path(cr, uid, "file://filestore", path)
cr, uid, 'file://filestore', path
)
def _register_hook(self, cr): def _register_hook(self, cr):
super(IrAttachment, self)._register_hook(cr) super(IrAttachment, self)._register_hook(cr)
@@ -101,7 +97,7 @@ class IrAttachment(osv.osv):
# done during the initialization. We need to move the attachments that # done during the initialization. We need to move the attachments that
# could have been created or updated in other addons before this addon # could have been created or updated in other addons before this addon
# was loaded # was loaded
update_module = load_modules_frame.f_locals.get('update_module') update_module = load_modules_frame.f_locals.get("update_module")
# We need to call the migration on the loading of the model because # We need to call the migration on the loading of the model because
# when we are upgrading addons, some of them might add attachments. # when we are upgrading addons, some of them might add attachments.
@@ -110,12 +106,12 @@ class IrAttachment(osv.osv):
# Typical example is images of ir.ui.menu which are updated in # Typical example is images of ir.ui.menu which are updated in
# ir.attachment at every upgrade of the addons # ir.attachment at every upgrade of the addons
if update_module: if update_module:
self.pool.get('ir.attachment')._force_storage_to_object_storage( self.pool.get("ir.attachment")._force_storage_to_object_storage(
cr, SUPERUSER_ID cr, SUPERUSER_ID
) )
def _save_in_db_anyway(self, cr, uid, ids, context=None): def _save_in_db_anyway(self, cr, uid, ids, context=None):
""" Return whether an attachment must be stored in db """Return whether an attachment must be stored in db
When we are using an Object Store. This is sometimes required When we are using an Object Store. This is sometimes required
because the object storage is slower than the database/filesystem. because the object storage is slower than the database/filesystem.
@@ -130,12 +126,11 @@ class IrAttachment(osv.osv):
an old database with attachments pointing to deleted assets. an old database with attachments pointing to deleted assets.
""" """
assert (isinstance(ids, int) or assert isinstance(ids, int) or len(ids) == 1, "Expecting only one record"
len(ids) == 1), 'Expecting only one record'
rec = self.browse(cr, uid, ids, context=context) rec = self.browse(cr, uid, ids, context=context)
# assets # assets
if rec.res_model == 'ir.ui.view': if rec.res_model == "ir.ui.view":
# assets are stored in 'ir.ui.view' # assets are stored in 'ir.ui.view'
return True return True
@@ -146,58 +141,50 @@ class IrAttachment(osv.osv):
# we keep them in the database instead of the object storage # we keep them in the database instead of the object storage
location = self._storage(cr, uid) location = self._storage(cr, uid)
for attach in self.browse(cr, uid, id, context): for attach in self.browse(cr, uid, id, context):
if (location in self._get_stores() and if location in self._get_stores() and self._save_in_db_anyway(
self._save_in_db_anyway(cr, uid, [id], context)): cr, uid, [id], context
):
# compute the fields that depend on datas # compute the fields that depend on datas
bin_data = value and value.decode('base64') or '' bin_data = value and value.decode("base64") or ""
vals = { vals = {
'file_size': len(bin_data), "file_size": len(bin_data),
'checksum': self._compute_checksum(bin_data), "checksum": self._compute_checksum(bin_data),
'db_datas': value, "db_datas": value,
# we seriously don't need index content on those fields # we seriously don't need index content on those fields
'index_content': False, "index_content": False,
'store_fname': False, "store_fname": False,
} }
fname = attach.store_fname fname = attach.store_fname
# write as superuser, as user probably does not # write as superuser, as user probably does not
# have write access # have write access
super(IrAttachment, self).write( super(IrAttachment, self).write(cr, SUPERUSER_ID, id, vals, context)
cr, SUPERUSER_ID, id, vals, context
)
if fname: if fname:
self._file_delete(cr, uid, fname) self._file_delete(cr, uid, fname)
continue continue
self._data_set(cr, uid, id, 'datas', value, None, context) self._data_set(cr, uid, id, "datas", value, None, context)
def _store_file_read(self, fname, bin_size=False): def _store_file_read(self, fname, bin_size=False):
storage = fname.partition('://')[0] storage = fname.partition("://")[0]
raise NotImplementedError( raise NotImplementedError("No implementation for %s" % (storage,))
'No implementation for %s' % (storage,)
)
def _store_file_write(self, storage, key, bin_data): def _store_file_write(self, storage, key, bin_data):
raise NotImplementedError( raise NotImplementedError("No implementation for %s" % (storage,))
'No implementation for %s' % (storage,)
)
def _store_file_delete(self, fname): def _store_file_delete(self, fname):
storage = fname.partition('://')[0] storage = fname.partition("://")[0]
raise NotImplementedError( raise NotImplementedError("No implementation for %s" % (storage,))
'No implementation for %s' % (storage,)
)
def _file_read(self, cr, uid, location, fname, bin_size=False): def _file_read(self, cr, uid, location, fname, bin_size=False):
if self._is_file_from_a_store(fname): if self._is_file_from_a_store(fname):
return self._store_file_read(fname, bin_size=bin_size) return self._store_file_read(fname, bin_size=bin_size)
else: else:
_super = super(IrAttachment, self) _super = super(IrAttachment, self)
return _super._file_read(cr, uid, location, return _super._file_read(cr, uid, location, fname, bin_size=bin_size)
fname, bin_size=bin_size)
def _file_write(self, cr, uid, location, value): def _file_write(self, cr, uid, location, value):
storage = self._storage(cr, uid) storage = self._storage(cr, uid)
if storage in self._get_stores(): if storage in self._get_stores():
bin_data = value.decode('base64') bin_data = value.decode("base64")
key = self._compute_checksum(bin_data) key = self._compute_checksum(bin_data)
filename = self._store_file_write(storage, key, bin_data) filename = self._store_file_write(storage, key, bin_data)
else: else:
@@ -209,8 +196,9 @@ class IrAttachment(osv.osv):
if self._is_file_from_a_store(fname): if self._is_file_from_a_store(fname):
# using SQL to include files hidden through unlink or due to record # using SQL to include files hidden through unlink or due to record
# rules # rules
cr.execute("SELECT COUNT(*) FROM ir_attachment " cr.execute(
"WHERE store_fname = %s", (fname,)) "SELECT COUNT(*) FROM ir_attachment " "WHERE store_fname = %s", (fname,)
)
count = cr.fetchone()[0] count = cr.fetchone()[0]
if int(count) == 1: if int(count) == 1:
self._store_file_delete(fname) self._store_file_delete(fname)
@@ -219,33 +207,31 @@ class IrAttachment(osv.osv):
def _is_file_from_a_store(self, fname): def _is_file_from_a_store(self, fname):
for store_name in self._get_stores(): for store_name in self._get_stores():
uri = '{}://'.format(store_name) uri = "{}://".format(store_name)
if fname.startswith(uri): if fname.startswith(uri):
return True return True
return False return False
def _move_attachment_to_store(self, cr, uid, ids, context=None): def _move_attachment_to_store(self, cr, uid, ids, context=None):
assert (isinstance(ids, int) or assert isinstance(ids, int) or len(ids) == 1, "Expecting only one record"
len(ids) == 1), 'Expecting only one record'
rec = self.browse(cr, uid, ids, context) rec = self.browse(cr, uid, ids, context)
_logger.info('inspecting attachment %s (%d)', rec.name, rec.id) _logger.info("inspecting attachment %s (%d)", rec.name, rec.id)
fname = rec.store_fname fname = rec.store_fname
if fname: if fname:
# migrating from filesystem filestore # migrating from filesystem filestore
# or from the old 'store_fname' without the bucket name # or from the old 'store_fname' without the bucket name
_logger.info('moving %s on the object storage', fname) _logger.info("moving %s on the object storage", fname)
self.write(cr, uid, ids, {'datas': rec.datas}, context) self.write(cr, uid, ids, {"datas": rec.datas}, context)
_logger.info('moved %s on the object storage', fname) _logger.info("moved %s on the object storage", fname)
return self._full_path(cr, uid, None, fname) return self._full_path(cr, uid, None, fname)
elif rec.db_datas: elif rec.db_datas:
_logger.info('moving on the object storage from database') _logger.info("moving on the object storage from database")
self.write(cr, uid, ids, {'datas': rec.datas}, context) self.write(cr, uid, ids, {"datas": rec.datas}, context)
def force_storage(self, cr, uid, context=None): def force_storage(self, cr, uid, context=None):
if not self._is_user_admin(cr, uid): if not self._is_user_admin(cr, uid):
raise except_orm( raise except_orm(
_('Error'), _("Error"), _("Only administrators can execute this action.")
_('Only administrators can execute this action.')
) )
storage = self._storage(cr, uid) storage = self._storage(cr, uid)
if storage not in self._get_stores(): if storage not in self._get_stores():
@@ -253,10 +239,10 @@ class IrAttachment(osv.osv):
self._force_storage_to_object_storage(cr, uid, context) self._force_storage_to_object_storage(cr, uid, context)
def _force_storage_to_object_storage(self, cr, uid, context=None): def _force_storage_to_object_storage(self, cr, uid, context=None):
_logger.info('migrating files to the object storage') _logger.info("migrating files to the object storage")
storage = self._storage(cr, uid) storage = self._storage(cr, uid)
domain = [('store_fname', 'not like', '{}://%'.format(storage))] domain = [("store_fname", "not like", "{}://%".format(storage))]
ids = self.search(cr, uid, domain, context=context) ids = self.search(cr, uid, domain, context=context)
files_to_clean = [] files_to_clean = []
@@ -273,7 +259,7 @@ class IrAttachment(osv.osv):
"WHERE id = %s " "WHERE id = %s "
"FOR UPDATE NOWAIT", "FOR UPDATE NOWAIT",
(attachment_id,), (attachment_id,),
log_exceptions=False log_exceptions=False,
) )
path = self._move_attachment_to_store( path = self._move_attachment_to_store(
@@ -282,8 +268,9 @@ class IrAttachment(osv.osv):
if path: if path:
files_to_clean.append(path) files_to_clean.append(path)
except psycopg2.OperationalError: except psycopg2.OperationalError:
_logger.error('Could not migrate attachment %s to %s' % _logger.error(
(attachment_id, storage)) "Could not migrate attachment %s to %s" % (attachment_id, storage)
)
def clean(): def clean():
clean_fs(files_to_clean) clean_fs(files_to_clean)
@@ -291,8 +278,8 @@ class IrAttachment(osv.osv):
# delete the files from the filesystem once we know the changes # delete the files from the filesystem once we know the changes
# have been committed in ir.attachment # have been committed in ir.attachment
if files_to_clean: if files_to_clean:
cr.after('commit', clean) cr.commit()
def _get_stores(self): def _get_stores(self):
""" To get the list of stores activated in the system """ """To get the list of stores activated in the system"""
return [] return []