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,7 +50,7 @@ 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):
@@ -60,26 +58,24 @@ class IrAttachment(osv.osv):
: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,7 +106,7 @@ 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
) )
@@ -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,7 +278,7 @@ 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"""