Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
exclude: |
(?x)
# NOT INSTALLABLE ADDONS
^fs_attachment/|
^fs_attachment_s3/|
^fs_file/|
^fs_folder/|
Expand Down
10 changes: 5 additions & 5 deletions fs_attachment/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ Base Attachment Object Store
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github
:target: https://github.com/OCA/storage/tree/18.0/fs_attachment
:target: https://github.com/OCA/storage/tree/19.0/fs_attachment
:alt: OCA/storage
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/storage-18-0/storage-18-0-fs_attachment
:target: https://translation.odoo-community.org/projects/storage-19-0/storage-19-0-fs_attachment
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=18.0
:target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=19.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|
Expand Down Expand Up @@ -422,7 +422,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues <https://github.com/OCA/storage/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/storage/issues/new?body=module:%20fs_attachment%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
`feedback <https://github.com/OCA/storage/issues/new?body=module:%20fs_attachment%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Expand Down Expand Up @@ -475,6 +475,6 @@ Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-lmignon|

This module is part of the `OCA/storage <https://github.com/OCA/storage/tree/18.0/fs_attachment>`_ project on GitHub.
This module is part of the `OCA/storage <https://github.com/OCA/storage/tree/19.0/fs_attachment>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
4 changes: 2 additions & 2 deletions fs_attachment/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{
"name": "Base Attachment Object Store",
"summary": "Store attachments on external object store",
"version": "18.0.2.1.0",
"version": "19.0.1.0.0",
"author": "Camptocamp, ACSONE SA/NV, Odoo Community Association (OCA)",
"license": "AGPL-3",
"development_status": "Beta",
Expand All @@ -17,7 +17,7 @@
"views/fs_storage.xml",
],
"external_dependencies": {"python": ["python_slugify", "fsspec>=2025.3.0"]},
"installable": False,
"installable": True,
"auto_install": False,
"maintainers": ["lmignon"],
"pre_init_hook": "pre_init_hook",
Expand Down
25 changes: 11 additions & 14 deletions fs_attachment/models/fs_file_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import threading
from contextlib import closing, contextmanager

from odoo import api, fields, models
from odoo import api, fields, models, modules
from odoo.sql_db import Cursor

_logger = logging.getLogger(__name__)
Expand All @@ -17,21 +17,18 @@ class FsFileGC(models.Model):
store_fname = fields.Char("Stored Filename")
fs_storage_code = fields.Char("Storage Code")

_sql_constraints = [
(
"store_fname_uniq",
"unique (store_fname)",
"The stored filename must be unique!",
),
]
_store_fname_uniq = models.Constraint(
"unique (store_fname)",
"The stored filename must be unique!",
)

def _is_test_mode(self) -> bool:
"""Return True if we are running the tests, so we do not mark files for
garbage collection into a separate transaction.
"""
return (
getattr(threading.current_thread(), "testing", False)
or self.env.registry.in_test_mode()
or modules.module.current_test
)

@contextmanager
Expand Down Expand Up @@ -101,7 +98,7 @@ def _gc_files(self) -> None:
# the LOCK statement will wait until those concurrent transactions end.
# But this transaction will not see the new attachements if it has done
# other requests before the LOCK (like the method _storage() above).
cr = self._cr
cr = self.env.cr
cr.commit() # pylint: disable=invalid-commit

# prevent all concurrent updates on ir_attachment and fs_file_gc
Expand All @@ -120,12 +117,12 @@ def _gc_files(self) -> None:
def _gc_files_unsafe(self) -> None:
# get the list of fs.storage codes that must be autovacuumed
codes = (
self.env["fs.storage"].search([]).filtered("autovacuum_gc").mapped("code")
self.env["fs.storage"].search([]).filtered("autovacuum_gc").mapped("code") # pylint: disable=no-search-all
)
if not codes:
return
# we process by batch of storage codes.
self._cr.execute(
self.env.cr.execute(
"""
SELECT
fs_storage_code,
Expand All @@ -145,7 +142,7 @@ def _gc_files_unsafe(self) -> None:
""",
(tuple(codes),),
)
for code, store_fnames in self._cr.fetchall():
for code, store_fnames in self.env.cr.fetchall():
self.env["fs.storage"].get_by_code(code)
fs = self.env["fs.storage"].get_fs_by_code(code)
for store_fname in store_fnames:
Expand All @@ -156,7 +153,7 @@ def _gc_files_unsafe(self) -> None:
_logger.debug("Failed to remove file %s", store_fname)

# delete the records from the table fs_file_gc
self._cr.execute(
self.env.cr.execute(
"""
DELETE FROM
fs_file_gc
Expand Down
12 changes: 6 additions & 6 deletions fs_attachment/models/fs_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from __future__ import annotations

from odoo import _, api, fields, models, tools
from odoo import api, fields, models, tools
from odoo.exceptions import ValidationError
from odoo.tools.safe_eval import const_eval

Expand Down Expand Up @@ -82,10 +82,10 @@ class FsStorage(models.Model):
def _check_use_as_default_for_attachments(self):
# constrains are checked in python since values can be provided by
# the server environment
defaults = self.search([]).filtered("use_as_default_for_attachments")
defaults = self.search([]).filtered("use_as_default_for_attachments") # pylint: disable=no-search-all
if len(defaults) > 1:
raise ValidationError(
_("Only one storage can be used as default for attachments")
self.env._("Only one storage can be used as default for attachments")
)

@property
Expand Down Expand Up @@ -165,7 +165,7 @@ def _check_force_db_for_default_attachment_rules(self):
continue
if not rec.use_as_default_for_attachments:
raise ValidationError(
_(
self.env._(
"The force_db_for_default_attachment_rules can only be set "
"if the storage is used as default for attachments."
)
Expand All @@ -174,7 +174,7 @@ def _check_force_db_for_default_attachment_rules(self):
const_eval(rec.force_db_for_default_attachment_rules)
except (SyntaxError, TypeError, ValueError) as e:
raise ValidationError(
_(
self.env._(
"The force_db_for_default_attachment_rules is not a valid "
"python dict."
)
Expand All @@ -184,7 +184,7 @@ def _check_force_db_for_default_attachment_rules(self):
@tools.ormcache()
def get_storage_code_for_attachments_fallback(self):
storages = (
self.sudo()
self.sudo() # pylint: disable=no-search-all
.search([])
.filtered_domain([("use_as_default_for_attachments", "=", True)])
)
Expand Down
75 changes: 44 additions & 31 deletions fs_attachment/models/ir_attachment.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
from contextlib import closing, contextmanager
from pathlib import Path

import fsspec # pylint: disable=missing-manifest-dependency
import fsspec
import psycopg2
from slugify import slugify # pylint: disable=missing-manifest-dependency
from slugify import slugify

import odoo
from odoo import _, api, fields, models
from odoo import api, fields, models
from odoo.exceptions import AccessError, UserError
from odoo.osv.expression import AND, OR, normalize_domain
from odoo.fields import Domain

from .strtobool import strtobool

Expand Down Expand Up @@ -169,9 +169,9 @@ def _store_in_db_instead_of_object_storage_domain(self):
for mimetype_key, limit in storage_config.items():
part = [("mimetype", "=like", f"{mimetype_key}%")]
if limit:
part = AND([part, [("file_size", "<=", limit)]])
part = Domain.AND([part, [("file_size", "<=", limit)]])
# OR simplifies to [(1, '=', 1)] if a domain being OR'ed is empty
domain = OR([domain, part]) if domain else part
domain = Domain.OR([domain, part]) if domain else part
return domain

def _store_in_db_instead_of_object_storage(self, data, mimetype):
Expand Down Expand Up @@ -224,22 +224,29 @@ def _store_in_db_instead_of_object_storage(self, data, mimetype):
return False

def _get_datas_related_values(self, data, mimetype):
values = super(
IrAttachment, self.with_context(mimetype=mimetype)
)._get_datas_related_values(data, mimetype)
storage = self.env.context.get("storage_location") or self._storage()
if data and storage in self._get_storage_codes():
if self._store_in_db_instead_of_object_storage(data, mimetype):
# compute the fields that depend on datas
bin_data = data
values = {
"file_size": len(bin_data),
"checksum": self._compute_checksum(bin_data),
"index_content": self._index(bin_data, mimetype),
"store_fname": False,
"db_datas": data,
}
return values
return super(
IrAttachment, self.with_context(mimetype=mimetype)
)._get_datas_related_values(data, mimetype)
# Force storing data in the database, overriding the filestore logic.
values.update(
{
"store_fname": False,
"db_datas": data,
}
)
else:
# Uses the full object storage path; standard Odoo uses a relative path.
path = self._get_fs_path(storage, data)
values.update(
{
"store_fname": f"{storage}://{path}",
"db_datas": False,
}
)
return values

###########################################################
# Odoo methods that we override to use the object storage #
Expand Down Expand Up @@ -306,7 +313,7 @@ def write(self, vals):
vals["mimetype"] = mimetypes[0]
else:
raise UserError(
_(
self.env._(
"You can't write on multiple attachments with different "
"mimetypes at the same time."
)
Expand Down Expand Up @@ -697,9 +704,10 @@ def _move_attachment_to_store(self):
self.ensure_one()
_logger.info("inspecting attachment %s (%d)", self.name, self.id)
fname = self.store_fname
storage = fname.partition("://")[0]
if self._is_storage_disabled(storage):
fname = False
if fname:
storage = fname.partition("://")[0]
if self._is_storage_disabled(storage):
fname = False
if fname:
# migrating from filesystem filestore
# or from the old 'store_fname' without the bucket name
Expand All @@ -723,7 +731,9 @@ def _move_attachment_to_store(self):
@api.model
def force_storage(self):
if not self.env["res.users"].browse(self.env.uid)._is_admin():
raise AccessError(_("Only administrators can execute this action."))
raise AccessError(
self.env._("Only administrators can execute this action.")
)
location = self.env.context.get("storage_location") or self._storage()
if location not in self._get_storage_codes():
return super().force_storage()
Expand Down Expand Up @@ -762,19 +772,22 @@ def force_storage_to_db_for_special_fields(
)
return

domain = AND(
domain = Domain.AND(
(
normalize_domain(
Domain.AND(
[
("store_fname", "=like", f"{storage}://%"),
Domain("store_fname", "=like", f"{storage}://%"),
# for res_field, see comment in
# _force_storage_to_object_storage
"|",
("res_field", "=", False),
("res_field", "!=", False),
Domain.OR(
[
Domain("res_field", "=", False),
Domain("res_field", "!=", False),
]
),
]
),
normalize_domain(self._store_in_db_instead_of_object_storage_domain()),
Domain(self._store_in_db_instead_of_object_storage_domain()),
)
)

Expand Down
34 changes: 17 additions & 17 deletions fs_attachment/models/ir_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,24 @@ class IrBinary(models.AbstractModel):
def _get_fs_attachment_for_field(self, record, field_name):
if record._name == "ir.attachment" and record.fs_filename:
return record

record.check_field_access_rights("read", [field_name])
field_def = record._fields[field_name]
if field_def.attachment and field_def.store:
fs_attachment = (
self.env["ir.attachment"]
.sudo()
.search(
domain=[
("res_model", "=", record._name),
("res_id", "=", record.id),
("res_field", "=", field_name),
],
limit=1,
field_def = record._fields.get(field_name)
if field_def:
record._check_field_access(field_def, "read")
if field_def.attachment and field_def.store:
fs_attachment = (
self.env["ir.attachment"]
.sudo()
.search(
domain=[
("res_model", "=", record._name),
("res_id", "=", record.id),
("res_field", "=", field_name),
],
limit=1,
)
)
)
if fs_attachment and fs_attachment.fs_filename:
return fs_attachment
if fs_attachment and fs_attachment.fs_filename:
return fs_attachment
return None

def _record_to_stream(self, record, field_name):
Expand Down
2 changes: 1 addition & 1 deletion fs_attachment/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
* Thierry Ducrest \<<[email protected]>\>
* Thierry Ducrest \<<[email protected]>\>
* Guewen Baconnier \<<[email protected]>\>
* Julien Coux \<<[email protected]>\>
* Akim Juillerat \<<[email protected]>\>
Expand Down
Loading
Loading