Skip to content

Commit e8b0061

Browse files
committed
[IMP] database_cleanup: purge orphaned attachments
Add wizard to list ir.attachment records whose store_fname points to a missing file on disk when storage is "file", and allow purging those rows. task 5258
1 parent 473dbfe commit e8b0061

13 files changed

Lines changed: 348 additions & 9 deletions

File tree

database_cleanup/README.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ Database cleanup
3232

3333
|badge1| |badge2| |badge3| |badge4| |badge5|
3434

35-
Clean your Odoo database from remnants of modules, models, columns and
36-
tables left by uninstalled modules (prior to 7.0) or a homebrew database
35+
Clean your Odoo database from remnants of modules, models, columns, tables and
36+
attachment records left by uninstalled modules (prior to 7.0) or a homebrew database
3737
upgrade to a new major version of Odoo.
3838

3939
Caution! This module is potentially harmful and can *easily* destroy the
@@ -52,7 +52,7 @@ Usage
5252

5353
After installation of this module, go to the Settings menu -> Technical ->
5454
Database cleanup. This menu is only available to members of the *Access Rights*
55-
group. Go through the modules, models, columns and tables
55+
group. Go through the modules, models, columns, tables and attachment
5656
entries under this menu (in that order) and find out if there is orphaned data
5757
in your database. You can either delete entries by line, or sweep all entries
5858
in one big step (if you are *really* confident).
@@ -88,6 +88,9 @@ Contributors
8888
* Mark Schuit <mark@gig.solutions>
8989
* `360ERP <https://www.360erp.com>`_:
9090
* Andrea Stirpe
91+
* `Cetmix <https://cetmix.com/>`_:
92+
* Ivan Sokolov
93+
* George Smirnov
9194

9295
Maintainers
9396
~~~~~~~~~~~

database_cleanup/__manifest__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"views/purge_data.xml",
2121
"views/create_indexes.xml",
2222
"views/purge_properties.xml",
23+
"views/purge_attachments.xml",
2324
"views/menu.xml",
2425
"security/ir.model.access.csv",
2526
],

database_cleanup/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from . import purge_fields
55
from . import purge_columns
66
from . import purge_tables
7+
from . import purge_attachments
78
from . import purge_data
89
from . import purge_menus
910
from . import create_indexes
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Copyright 2026 Cetmix
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3+
4+
import os
5+
6+
from odoo import _, api, fields, models
7+
from odoo.exceptions import AccessError, UserError, ValidationError
8+
9+
REASON_MISSING_FILE = "missing_file"
10+
11+
12+
class CleanupPurgeLineAttachment(models.TransientModel):
13+
_inherit = "cleanup.purge.line"
14+
_name = "cleanup.purge.line.attachment"
15+
_description = "Cleanup Purge Line Attachment"
16+
17+
attachment_id = fields.Many2one("ir.attachment")
18+
reason = fields.Selection(
19+
[
20+
(REASON_MISSING_FILE, "File missing in filestore"),
21+
],
22+
)
23+
error_message = fields.Char(readonly=True)
24+
wizard_id = fields.Many2one("cleanup.purge.wizard.attachment", readonly=True)
25+
26+
def purge(self):
27+
"""Unlink orphaned attachment records upon manual confirmation.
28+
29+
Filters unpurged lines with attachment_id. Unlinks each attachment
30+
individually; failures are logged and skipped so the batch continues.
31+
Only successfully removed attachments get their lines marked purged.
32+
33+
:return: result of write({"purged": True}) on successfully purged lines,
34+
or True if none were purged
35+
"""
36+
if self:
37+
objs = self
38+
else:
39+
objs = self.env["cleanup.purge.line.attachment"].browse(
40+
self._context.get("active_ids")
41+
)
42+
to_unlink = objs.filtered(lambda x: not x.purged and x.attachment_id)
43+
self.logger.info("Purging attachments: %s", to_unlink.mapped("name"))
44+
purged_line_ids = []
45+
for line in to_unlink:
46+
attach = line.attachment_id
47+
try:
48+
attach.unlink()
49+
purged_line_ids.append(line.id)
50+
except (UserError, ValidationError, AccessError) as exc:
51+
self.logger.warning(
52+
"Attachment #%s cannot be deleted: %s",
53+
attach.id,
54+
str(exc),
55+
)
56+
line.error_message = str(exc)
57+
if not purged_line_ids:
58+
return True
59+
return (
60+
self.env["cleanup.purge.line.attachment"]
61+
.browse(purged_line_ids)
62+
.write({"purged": True})
63+
)
64+
65+
66+
class CleanupPurgeWizardAttachment(models.TransientModel):
67+
_inherit = "cleanup.purge.wizard"
68+
_name = "cleanup.purge.wizard.attachment"
69+
_description = "Purge attachments"
70+
71+
@api.model
72+
def find(self):
73+
"""Collect ir.attachment records whose backing files are missing on disk.
74+
75+
Requires file storage. Searches binary attachments with store_fname,
76+
checks each file exists via os.path.isfile(_full_path(store_fname)).
77+
78+
:raises UserError: if storage != "file" or no orphaned entries found
79+
"""
80+
if self.env["ir.attachment"]._storage() != "file":
81+
raise UserError(
82+
_(
83+
"Attachment storage is not 'file'. "
84+
"Purge of orphaned attachments only works with file storage."
85+
)
86+
)
87+
res = []
88+
attachments = self.env["ir.attachment"].search(
89+
[
90+
("store_fname", "!=", False),
91+
("type", "=", "binary"),
92+
]
93+
)
94+
for attach in attachments:
95+
full_path = self.env["ir.attachment"]._full_path(attach.store_fname)
96+
if not os.path.isfile(full_path):
97+
res.append(
98+
fields.Command.create(
99+
{
100+
"attachment_id": attach.id,
101+
"name": attach.store_fname or attach.name or str(attach.id),
102+
"reason": REASON_MISSING_FILE,
103+
}
104+
)
105+
)
106+
if not res:
107+
raise UserError(_("No orphaned attachment entries found"))
108+
return res
109+
110+
purge_line_ids = fields.One2many("cleanup.purge.line.attachment", "wizard_id")

database_cleanup/readme/CONTRIBUTORS.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
* Mark Schuit <mark@gig.solutions>
55
* `360ERP <https://www.360erp.com>`_:
66
* Andrea Stirpe
7+
* `Cetmix <https://cetmix.com/>`_:
8+
* Ivan Sokolov
9+
* George Smirnov

database_cleanup/readme/DESCRIPTION.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Clean your Odoo database from remnants of modules, models, columns and
2-
tables left by uninstalled modules (prior to 7.0) or a homebrew database
1+
Clean your Odoo database from remnants of modules, models, columns, tables and
2+
attachment records left by uninstalled modules (prior to 7.0) or a homebrew database
33
upgrade to a new major version of Odoo.
44

55
Caution! This module is potentially harmful and can *easily* destroy the

database_cleanup/readme/USAGE.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
After installation of this module, go to the Settings menu -> Technical ->
22
Database cleanup. This menu is only available to members of the *Access Rights*
3-
group. Go through the modules, models, columns and tables
3+
group. Go through the modules, models, columns, tables and attachment
44
entries under this menu (in that order) and find out if there is orphaned data
55
in your database. You can either delete entries by line, or sweep all entries
66
in one big step (if you are *really* confident).

database_cleanup/security/ir.model.access.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ access_cleanup_purge_line_menu,access_cleanup_purge_line_menu,model_cleanup_purg
1717
access_cleanup_purge_wizard_menu,access_cleanup_purge_wizard_menu,model_cleanup_purge_wizard_menu,base.group_user,1,1,1,1
1818
access_cleanup_purge_line_property,access_cleanup_purge_line_property,model_cleanup_purge_line_property,base.group_user,1,1,1,1
1919
access_cleanup_purge_wizard_property,access_cleanup_purge_wizard_property,model_cleanup_purge_wizard_property,base.group_user,1,1,1,1
20+
access_cleanup_purge_line_attachment,access_cleanup_purge_line_attachment,model_cleanup_purge_line_attachment,base.group_user,1,1,1,1
21+
access_cleanup_purge_wizard_attachment,access_cleanup_purge_wizard_attachment,model_cleanup_purge_wizard_attachment,base.group_user,1,1,1,1

database_cleanup/static/description/index.html

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -375,8 +375,8 @@ <h1>Database cleanup</h1>
375375
!! source digest: sha256:f2204c1d994e5d6dc3dc0ee765ef5b3d735612c05c4a8d467bab9d9bedd99872
376376
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
377377
<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/server-tools/tree/16.0/database_cleanup"><img alt="OCA/server-tools" src="https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-database_cleanup"><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/server-tools&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
378-
<p>Clean your Odoo database from remnants of modules, models, columns and
379-
tables left by uninstalled modules (prior to 7.0) or a homebrew database
378+
<p>Clean your Odoo database from remnants of modules, models, columns, tables and
379+
attachment records left by uninstalled modules (prior to 7.0) or a homebrew database
380380
upgrade to a new major version of Odoo.</p>
381381
<p>Caution! This module is potentially harmful and can <em>easily</em> destroy the
382382
integrity of your data. Do not use if you are not entirely comfortable
@@ -400,7 +400,7 @@ <h1>Database cleanup</h1>
400400
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
401401
<p>After installation of this module, go to the Settings menu -&gt; Technical -&gt;
402402
Database cleanup. This menu is only available to members of the <em>Access Rights</em>
403-
group. Go through the modules, models, columns and tables
403+
group. Go through the modules, models, columns, tables and attachment
404404
entries under this menu (in that order) and find out if there is orphaned data
405405
in your database. You can either delete entries by line, or sweep all entries
406406
in one big step (if you are <em>really</em> confident).</p>
@@ -439,6 +439,9 @@ <h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
439439
</dd>
440440
</dl>
441441
</li>
442+
<li><a class="reference external" href="https://cetmix.com/">Cetmix</a>:
443+
* Ivan Sokolov
444+
* George Smirnov</li>
442445
</ul>
443446
</div>
444447
<div class="section" id="maintainers">

database_cleanup/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from . import common
55
from . import test_create_indexes
66
from . import test_identifier_adapter
7+
from . import test_purge_attachments
78
from . import test_purge_columns
89
from . import test_purge_data
910
from . import test_purge_fields

0 commit comments

Comments
 (0)