Skip to content
Closed
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
71 changes: 42 additions & 29 deletions fs_attachment/models/ir_attachment.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from slugify import slugify # pylint: disable=missing-manifest-dependency

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
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 \<<thierry.ducrest@camptocamp.com>\>
* Thierry Ducrest \<<thierry.ducrest@camptocamp.com>\>
* Guewen Baconnier \<<guewen.baconnier@camptocamp.com>\>
* Julien Coux \<<julien.coux@camptocamp.com>\>
* Akim Juillerat \<<akim.juillerat@camptocamp.com>\>
Expand Down
6 changes: 3 additions & 3 deletions fs_attachment/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ <h1>Base Attachment Object Store</h1>
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:ce5c43b22c654c05f7c0b6c6b919001cb4a4c77d4031841a7a9acc02a9b2d1e0
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/storage/tree/18.0/fs_attachment"><img alt="OCA/storage" src="https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/storage-18-0/storage-18-0-fs_attachment"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/storage&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/storage/tree/19.0/fs_attachment"><img alt="OCA/storage" src="https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/storage-19-0/storage-19-0-fs_attachment"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/storage&amp;target_branch=19.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>In some cases, you need to store attachment in another system that the
Odoo’s filestore. For example, when your deployment is based on a
multi-server architecture to ensure redundancy and scalability, your
Expand Down Expand Up @@ -772,7 +772,7 @@ <h2><a class="toc-backref" href="#toc-entry-15">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/storage/issues">GitHub Issues</a>.
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
<a class="reference external" href="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</a>.</p>
<a class="reference external" href="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**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
Expand Down Expand Up @@ -815,7 +815,7 @@ <h3><a class="toc-backref" href="#toc-entry-19">Maintainers</a></h3>
promote its widespread use.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/lmignon"><img alt="lmignon" src="https://github.com/lmignon.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/storage/tree/18.0/fs_attachment">OCA/storage</a> project on GitHub.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/storage/tree/19.0/fs_attachment">OCA/storage</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
Expand Down
22 changes: 22 additions & 0 deletions fs_attachment/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import shutil
import tempfile

from odoo.fields import Command
from odoo.tests.common import TransactionCase


Expand All @@ -13,6 +14,13 @@ def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
temp_dir = tempfile.mkdtemp()
cls.default_backend = cls.env["fs.storage"].create(
{
"name": "Odoo Filesystem Backend",
"protocol": "odoofs",
"code": "odoofs",
}
)
cls.temp_backend = cls.env["fs.storage"].create(
{
"name": "Temp FS Storage",
Expand All @@ -33,6 +41,20 @@ def setUpClass(cls):
cls.temp_dir = temp_dir
cls.gc_file_model = cls.env["fs.file.gc"]
cls.ir_attachment_model = cls.env["ir.attachment"]
cls.demo_user = (
cls.env["res.users"]
.with_context(no_reset_password=True)
.create(
{
"name": "Test User",
"login": "demo",
"password": "demo",
"email": "test@yourcompany.com",
"company_id": cls.env.ref("base.main_company").id,
"group_ids": [Command.link(cls.env.ref("base.group_user").id)],
}
)
)

@cls.addClassCleanup
def cleanup_tempdir():
Expand Down
Loading
Loading