diff --git a/fs_attachment/README.rst b/fs_attachment/README.rst index be909f69c9..83ee74bc07 100644 --- a/fs_attachment/README.rst +++ b/fs_attachment/README.rst @@ -160,6 +160,26 @@ configure the way attachments will be stored in the filesystem. - application/javascript are stored in database whatever their size - text/css are stored in database whatever their size + The rules can also be grouped by rule name: + + - ``mimetype-size``: the mimetype rules described above + - ``field-size``: the key is a ``.`` pair, to force the + storage in database of the attachments of a specific binary field, + whatever their mimetype + + It's useful for fields like the menu icons, that are read on every + page load, without having to keep all the images in database: + + {"mimetype-size": {"image/": 51200, "application/javascript": 0, + "text/css": 0}, "field-size": {"ir.ui.menu.web_icon_data": 0}} + + Keys that are not a rule name are read as mimetype rules, so existing + configurations keep working. + + A field rule is checked before the mimetype rules: if the attachment + matches it and is within its limit, it is stored in database, + otherwise the mimetype rules apply. + This option is only available on the filesystem storage that is used as default for attachments. diff --git a/fs_attachment/models/fs_storage.py b/fs_attachment/models/fs_storage.py index fdffe76e15..4e399a81aa 100644 --- a/fs_attachment/models/fs_storage.py +++ b/fs_attachment/models/fs_storage.py @@ -66,7 +66,11 @@ class FsStorage(models.Model): "* images mimetypes (image/png, image/jpeg, ...) below 50KB are stored " "in database\n" "* application/javascript are stored in database whatever their size \n" - "* text/css are stored in database whatever their size", + "* text/css are stored in database whatever their size\n" + "The rules can also be grouped by rule name: 'mimetype-size' for the " + "mimetype rules above and 'field-size' with '.' keys (e.g. " + "'ir.ui.menu.web_icon_data') to force the storage in database of the " + "attachments of a specific binary field, whatever their mimetype.", default=lambda self: self._default_force_db_for_default_attachment_rules, ) use_filename_obfuscation = fields.Boolean( @@ -155,7 +159,7 @@ def _check_force_db_for_default_attachment_rules(self): ) ) try: - const_eval(rec.force_db_for_default_attachment_rules) + rules = const_eval(rec.force_db_for_default_attachment_rules) except (SyntaxError, TypeError, ValueError) as e: raise ValidationError( self.env._( @@ -163,6 +167,32 @@ def _check_force_db_for_default_attachment_rules(self): "python dict." ) ) from e + if not isinstance(rules, dict): + raise ValidationError( + self.env._( + "The force_db_for_default_attachment_rules is not a valid " + "python dict." + ) + ) + for rule_name in ("mimetype-size", "field-size"): + if not isinstance(rules.get(rule_name, {}), dict): + raise ValidationError( + self.env._( + "The '%(rule_name)s' rules of " + "force_db_for_default_attachment_rules must be a dict.", + rule_name=rule_name, + ) + ) + for field_key in rules.get("field-size", {}): + if "." not in field_key: + raise ValidationError( + self.env._( + "The 'field-size' key '%(key)s' of " + "force_db_for_default_attachment_rules must be a " + "'.' pair.", + key=field_key, + ) + ) @api.model @tools.ormcache() diff --git a/fs_attachment/models/ir_attachment.py b/fs_attachment/models/ir_attachment.py index 3227f7c7c7..8c0deeef66 100644 --- a/fs_attachment/models/ir_attachment.py +++ b/fs_attachment/models/ir_attachment.py @@ -152,6 +152,27 @@ def _get_storage_force_db_config(self): self._storage() ) + def _get_storage_force_db_rules(self): + """Return the force db config grouped by rule name + + The config maps a rule name to its rules: + + * ``mimetype-size``: ``{mimetype_key: limit}`` + * ``field-size``: ``{".": limit}`` + + Any other key is a legacy ``mimetype_key: limit`` entry and is merged + into the ``mimetype-size`` rules. + + :return: a dict ``{rule_name: rules}`` with all the known rule names + """ + rules = {"mimetype-size": {}, "field-size": {}} + for key, value in self._get_storage_force_db_config().items(): + if key in rules: + rules[key].update(value) + else: + rules["mimetype-size"][key] = value + return rules + def _store_in_db_instead_of_object_storage_domain(self): """Return a domain for attachments that must be forced to DB @@ -165,13 +186,19 @@ def _store_in_db_instead_of_object_storage_domain(self): ``_store_in_db_instead_of_object_storage``. """ domain = [] - storage_config = self._get_storage_force_db_config() - for mimetype_key, limit in storage_config.items(): + rules = self._get_storage_force_db_rules() + for mimetype_key, limit in rules["mimetype-size"].items(): part = [("mimetype", "=like", f"{mimetype_key}%")] if limit: part = Domain.AND([part, [("file_size", "<=", limit)]]) # OR simplifies to [(1, '=', 1)] if a domain being OR'ed is empty domain = Domain.OR([domain, part]) if domain else part + for field_key, limit in rules["field-size"].items(): + res_model, res_field = field_key.rsplit(".", 1) + part = [("res_model", "=", res_model), ("res_field", "=", res_field)] + if limit: + part = Domain.AND([part, [("file_size", "<=", limit)]]) + domain = Domain.OR([domain, part]) if domain else part return domain def _store_in_db_instead_of_object_storage(self, data, mimetype): @@ -204,6 +231,21 @@ def _store_in_db_instead_of_object_storage(self, data, mimetype): value is the limit in size below which attachments are kept in DB. 0 means no limit. + The rules can also be grouped by rule name, which allows other kinds + of rules. ``mimetype-size`` holds the mimetype rules described above + and ``field-size`` forces the storage in DB of the attachments of a + specific binary field, whatever their mimetype, for instance:: + + { + "mimetype-size": {"image/": 51200}, + "field-size": {"ir.ui.menu.web_icon_data": 0}, + } + + Keys that are not a rule name are read as mimetype rules, so both + forms can be mixed. A field rule is checked before the mimetype rules: + if the attachment matches it and is within its limit, it is stored in + DB, otherwise the mimetype rules apply. + These limits are applied only if the storage is the default one for attachments (see ``_storage``). @@ -214,8 +256,15 @@ def _store_in_db_instead_of_object_storage(self, data, mimetype): """ if self._is_storage_disabled(): return True - storage_config = self._get_storage_force_db_config() - for mimetype_key, limit in storage_config.items(): + rules = self._get_storage_force_db_rules() + res_model = self.env.context.get("attachment_res_model") + res_field = self.env.context.get("attachment_res_field") + field_key = f"{res_model}.{res_field}" + if res_model and res_field and field_key in rules["field-size"]: + limit = rules["field-size"][field_key] + if not limit or len(data) <= limit: + return True + for mimetype_key, limit in rules["mimetype-size"].items(): if mimetype.startswith(mimetype_key): if not limit: return True diff --git a/fs_attachment/readme/USAGE.md b/fs_attachment/readme/USAGE.md index 6283ddb40d..669cb02550 100644 --- a/fs_attachment/readme/USAGE.md +++ b/fs_attachment/readme/USAGE.md @@ -70,6 +70,26 @@ configure the way attachments will be stored in the filesystem. - application/javascript are stored in database whatever their size - text/css are stored in database whatever their size + The rules can also be grouped by rule name: + + - `mimetype-size`: the mimetype rules described above + - `field-size`: the key is a `.` pair, to force the + storage in database of the attachments of a specific binary field, + whatever their mimetype + + It's useful for fields like the menu icons, that are read on every + page load, without having to keep all the images in database: + + > {"mimetype-size": {"image/": 51200, "application/javascript": 0, + > "text/css": 0}, "field-size": {"ir.ui.menu.web_icon_data": 0}} + + Keys that are not a rule name are read as mimetype rules, so existing + configurations keep working. + + A field rule is checked before the mimetype rules: if the attachment + matches it and is within its limit, it is stored in database, + otherwise the mimetype rules apply. + This option is only available on the filesystem storage that is used as default for attachments. diff --git a/fs_attachment/readme/newsfragments/force_db_field.feature b/fs_attachment/readme/newsfragments/force_db_field.feature new file mode 100644 index 0000000000..f477b0d15c --- /dev/null +++ b/fs_attachment/readme/newsfragments/force_db_field.feature @@ -0,0 +1,3 @@ +Allow to group the `force_db_for_default_attachment_rules` by rule name and +add a `field-size` rule to keep the attachments of a specific binary field (e.g. +`ir.ui.menu.web_icon_data`) in the database whatever their mimetype. diff --git a/fs_attachment/static/description/index.html b/fs_attachment/static/description/index.html index 561f035b25..cc0f1aa191 100644 --- a/fs_attachment/static/description/index.html +++ b/fs_attachment/static/description/index.html @@ -3,7 +3,7 @@ -Base Attachment Object Store +README.rst