diff --git a/fs_storage_backup/README.rst b/fs_storage_backup/README.rst new file mode 100644 index 0000000000..29b792d90a --- /dev/null +++ b/fs_storage_backup/README.rst @@ -0,0 +1,103 @@ +========================= +Filesystem Storage Backup +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3292ef4f97f5dcf8a5364cba204599da2169dd30020b9b488ddb72885f85f85b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :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_storage_backup + :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_storage_backup + :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 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +With this module you can configure one or more database backup +locations. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +1. Go to Settings > Technical > FS Storage > FS Storage +2. Select a filesystem you want to use for backups. **NOTE: Make sure + you don't use the filestore as backup location otherwise it's + possible you'll back up the backup** +3. Enable ``Use For Backups`` +4. Follow it (using the chatter) if you want to get notified when a + backup fails +5. To know if the backup is working correctly you can run the scheduled + action (``Backup database and delete old backups``) manually to test + it. + +Usage +===== + +The backup is done automatically by a scheduled action +(``Backup database and delete old backups``). + +Known issues / Roadmap +====================== + +- **Configurable backup frequency**: e.g. backup every 7 days in s3 and + every 4 hours on a FTP server. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Onestein + +Contributors +------------ + +- Dennis Sluijk d.sluijk@onestein.nl (https://onestein.nl) + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fs_storage_backup/__init__.py b/fs_storage_backup/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/fs_storage_backup/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/fs_storage_backup/__manifest__.py b/fs_storage_backup/__manifest__.py new file mode 100644 index 0000000000..7e8ffb9a33 --- /dev/null +++ b/fs_storage_backup/__manifest__.py @@ -0,0 +1,14 @@ +{ + "name": "Filesystem Storage Backup", + "category": "Technical", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Onestein, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/storage", + "depends": ["fs_storage", "mail"], + "data": [ + "data/ir_cron_data.xml", + "data/mail_message_subtype_data.xml", + "views/fs_storage_view.xml", + ], +} diff --git a/fs_storage_backup/data/ir_cron_data.xml b/fs_storage_backup/data/ir_cron_data.xml new file mode 100644 index 0000000000..caa841405e --- /dev/null +++ b/fs_storage_backup/data/ir_cron_data.xml @@ -0,0 +1,16 @@ + + + + Backup database and delete old backups + 1 + days + True + + code + model.cron_backup_db() + + + diff --git a/fs_storage_backup/data/mail_message_subtype_data.xml b/fs_storage_backup/data/mail_message_subtype_data.xml new file mode 100644 index 0000000000..e56ff2c210 --- /dev/null +++ b/fs_storage_backup/data/mail_message_subtype_data.xml @@ -0,0 +1,15 @@ + + + + Backup Failed + Backup failed + fs.storage + + + + Backup Cleanup Failed + Failed to clean up old backups + fs.storage + + + diff --git a/fs_storage_backup/i18n/fs_storage_backup.pot b/fs_storage_backup/i18n/fs_storage_backup.pot new file mode 100644 index 0000000000..15799348b4 --- /dev/null +++ b/fs_storage_backup/i18n/fs_storage_backup.pot @@ -0,0 +1,177 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage_backup +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: fs_storage_backup +#: model:mail.message.subtype,name:fs_storage_backup.message_subtype_cleanup_failed +msgid "Backup Cleanup Failed" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_dir +msgid "Backup Directory" +msgstr "" + +#. module: fs_storage_backup +#: model:mail.message.subtype,name:fs_storage_backup.message_subtype_backup_failed +msgid "Backup Failed" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_filename_format +msgid "Backup Filename" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.actions.server,name:fs_storage_backup.cron_backup_db_ir_actions_server +#: model:ir.cron,cron_name:fs_storage_backup.cron_backup_db +msgid "Backup database and delete old backups" +msgstr "" + +#. module: fs_storage_backup +#: model:mail.message.subtype,description:fs_storage_backup.message_subtype_backup_failed +msgid "Backup failed" +msgstr "" + +#. module: fs_storage_backup +#: model_terms:ir.ui.view,arch_db:fs_storage_backup.fs_storage_form_view +msgid "Backups" +msgstr "" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#, python-format +msgid "Database backup failed" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model,name:fs_storage_backup.model_fs_storage +msgid "FS Storage" +msgstr "" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#: model:mail.message.subtype,description:fs_storage_backup.message_subtype_cleanup_failed +#, python-format +msgid "Failed to clean up old backups" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__has_message +msgid "Has Message" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_include_filestore +msgid "Include Filestore In Backup" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_keep_time +msgid "Keep backups of (in days)" +msgstr "" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#, python-format +msgid "Keep backups of (in days) must be greater or than 0." +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_ids +msgid "Messages" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__use_for_backup +msgid "Use For Backups" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__website_message_ids +msgid "Website communication history" +msgstr "" diff --git a/fs_storage_backup/i18n/it.po b/fs_storage_backup/i18n/it.po new file mode 100644 index 0000000000..e571d295b1 --- /dev/null +++ b/fs_storage_backup/i18n/it.po @@ -0,0 +1,178 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage_backup +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: fs_storage_backup +#: model:mail.message.subtype,name:fs_storage_backup.message_subtype_cleanup_failed +msgid "Backup Cleanup Failed" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_dir +msgid "Backup Directory" +msgstr "" + +#. module: fs_storage_backup +#: model:mail.message.subtype,name:fs_storage_backup.message_subtype_backup_failed +msgid "Backup Failed" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_filename_format +msgid "Backup Filename" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.actions.server,name:fs_storage_backup.cron_backup_db_ir_actions_server +#: model:ir.cron,cron_name:fs_storage_backup.cron_backup_db +msgid "Backup database and delete old backups" +msgstr "" + +#. module: fs_storage_backup +#: model:mail.message.subtype,description:fs_storage_backup.message_subtype_backup_failed +msgid "Backup failed" +msgstr "" + +#. module: fs_storage_backup +#: model_terms:ir.ui.view,arch_db:fs_storage_backup.fs_storage_form_view +msgid "Backups" +msgstr "" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#, python-format +msgid "Database backup failed" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model,name:fs_storage_backup.model_fs_storage +msgid "FS Storage" +msgstr "" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#: model:mail.message.subtype,description:fs_storage_backup.message_subtype_cleanup_failed +#, python-format +msgid "Failed to clean up old backups" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__has_message +msgid "Has Message" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_include_filestore +msgid "Include Filestore In Backup" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_keep_time +msgid "Keep backups of (in days)" +msgstr "" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#, python-format +msgid "Keep backups of (in days) must be greater or than 0." +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_ids +msgid "Messages" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__use_for_backup +msgid "Use For Backups" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__website_message_ids +msgid "Website communication history" +msgstr "" diff --git a/fs_storage_backup/models/__init__.py b/fs_storage_backup/models/__init__.py new file mode 100644 index 0000000000..349bb0495a --- /dev/null +++ b/fs_storage_backup/models/__init__.py @@ -0,0 +1 @@ +from . import fs_storage diff --git a/fs_storage_backup/models/fs_storage.py b/fs_storage_backup/models/fs_storage.py new file mode 100644 index 0000000000..f7a26350ea --- /dev/null +++ b/fs_storage_backup/models/fs_storage.py @@ -0,0 +1,118 @@ +import logging +import traceback +from datetime import timedelta, timezone +from os import path + +from odoo import _, api, fields, models, tools +from odoo.exceptions import ValidationError +from odoo.service import db + +_logger = logging.getLogger(__name__) + + +class FSStorage(models.Model): + _name = "fs.storage" + _inherit = ["fs.storage", "mail.thread"] + + use_for_backup = fields.Boolean(string="Use For Backups") + backup_include_filestore = fields.Boolean( + string="Include Filestore In Backup", + ) + backup_filename_format = fields.Char( + string="Backup Filename", default="backup-%(db)s-%(dt)s.%(ext)s" + ) + backup_keep_time = fields.Integer(string="Keep backups of (in days)", default=7) + backup_dir = fields.Char(string="Backup Directory", default="backups") + + @property + def _server_env_fields(self): + env_fields = super()._server_env_fields + env_fields.update( + { + "use_for_backup": {}, + "backup_include_filestore": {}, + "backup_filename_format": {"no_default_field": False}, + "backup_keep_time": {"no_default_field": False}, + "backup_dir": {"no_default_field": False}, + } + ) + return env_fields + + @api.constrains("backup_keep_time") + def _constrain_backup_keep_time(self): + if self.backup_keep_time < 1: + raise ValidationError( + _("Keep backups of (in days) must be greater or than 0.") + ) + + def _get_backup_format(self): + self.ensure_one() + return self.backup_include_filestore and "zip" or "dump" + + def _get_backup_path(self): + self.ensure_one() + file_ext = self._get_backup_format() + current_datetime = fields.Datetime.now().strftime("%Y%m%d%H%M%S") + return path.join( + self.backup_dir, + self.backup_filename_format + % {"db": self.env.cr.dbname, "dt": current_datetime, "ext": file_ext}, + ) + + def backup_db(self): + self.ensure_one() + try: + backup_path = self._get_backup_path() + self.fs.makedirs(self.backup_dir, exist_ok=True) + if self.fs.exists(backup_path): + raise Exception(f"File already exists ({backup_path}).") + backup_file = self.fs.open(backup_path, "w") + list_db = tools.config["list_db"] + if not list_db: + tools.config["list_db"] = True + db.dump_db( + self.env.cr.dbname, + backup_file.buffer, + backup_format=self._get_backup_format(), + ) + tools.config["list_db"] = list_db + except Exception as e: + _logger.exception("Database backup failed: %s", e) + self.message_post( + subject=_("Database backup failed"), + body=f"
{tools.html_escape(traceback.format_exc())}
", + body_is_html=True, + subtype_id=self.env.ref( + "fs_storage_backup.message_subtype_backup_failed" + ).id, + ) + + def cleanup_old_backups(self): + self.ensure_one() + expiry_date = fields.Datetime.now() - timedelta(days=self.backup_keep_time) + try: + files = self.fs.ls(self.backup_dir, detail=False) + for file_path in files: + file_dt = self.fs.modified(file_path) + file_dt = file_dt.astimezone(timezone.utc) + file_dt = file_dt.replace(tzinfo=None) + if file_dt < expiry_date: + self.fs.rm(file_path) + except Exception as e: + _logger.exception("Failed to clean up old backups: %s", e) + self.message_post( + subject=_("Failed to clean up old backups"), + body=f"
{tools.html_escape(traceback.format_exc())}
", + body_is_html=True, + subtype_id=self.env.ref( + "fs_storage_backup.message_subtype_cleanup_failed" + ).id, + ) + + @api.model + def cron_backup_db(self): + # use_for_backup is not searchable + storages = self.search([]) + for storage in storages.filtered(lambda s: s.use_for_backup): + storage.backup_db() + storage.cleanup_old_backups() diff --git a/fs_storage_backup/pyproject.toml b/fs_storage_backup/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/fs_storage_backup/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fs_storage_backup/readme/CONFIGURE.md b/fs_storage_backup/readme/CONFIGURE.md new file mode 100644 index 0000000000..8d674feed7 --- /dev/null +++ b/fs_storage_backup/readme/CONFIGURE.md @@ -0,0 +1,7 @@ +1. Go to Settings > Technical > FS Storage > FS Storage +2. Select a filesystem you want to use for backups. + **NOTE: Make sure you don't use the filestore as backup location otherwise it's possible you'll back up the backup** +3. Enable `Use For Backups` +4. Follow it (using the chatter) if you want to get notified when a backup fails +5. To know if the backup is working correctly you can run the scheduled action + (`Backup database and delete old backups`) manually to test it. diff --git a/fs_storage_backup/readme/CONTRIBUTORS.md b/fs_storage_backup/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..8a94e6002f --- /dev/null +++ b/fs_storage_backup/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Dennis Sluijk (https://onestein.nl) diff --git a/fs_storage_backup/readme/DESCRIPTION.md b/fs_storage_backup/readme/DESCRIPTION.md new file mode 100644 index 0000000000..25f1c1b1fa --- /dev/null +++ b/fs_storage_backup/readme/DESCRIPTION.md @@ -0,0 +1 @@ +With this module you can configure one or more database backup locations. \ No newline at end of file diff --git a/fs_storage_backup/readme/ROADMAP.md b/fs_storage_backup/readme/ROADMAP.md new file mode 100644 index 0000000000..9727d47a68 --- /dev/null +++ b/fs_storage_backup/readme/ROADMAP.md @@ -0,0 +1 @@ +- **Configurable backup frequency**: e.g. backup every 7 days in s3 and every 4 hours on a FTP server. \ No newline at end of file diff --git a/fs_storage_backup/readme/USAGE.md b/fs_storage_backup/readme/USAGE.md new file mode 100644 index 0000000000..be546dce1e --- /dev/null +++ b/fs_storage_backup/readme/USAGE.md @@ -0,0 +1 @@ +The backup is done automatically by a scheduled action (`Backup database and delete old backups`). \ No newline at end of file diff --git a/fs_storage_backup/static/description/icon.png b/fs_storage_backup/static/description/icon.png new file mode 100644 index 0000000000..1dcc49c24f Binary files /dev/null and b/fs_storage_backup/static/description/icon.png differ diff --git a/fs_storage_backup/static/description/index.html b/fs_storage_backup/static/description/index.html new file mode 100644 index 0000000000..81453ac1df --- /dev/null +++ b/fs_storage_backup/static/description/index.html @@ -0,0 +1,454 @@ + + + + + +Filesystem Storage Backup + + + +
+

Filesystem Storage Backup

+ + +

Beta License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

With this module you can configure one or more database backup +locations.

+

Table of contents

+ +
+

Configuration

+
    +
  1. Go to Settings > Technical > FS Storage > FS Storage
  2. +
  3. Select a filesystem you want to use for backups. NOTE: Make sure +you don’t use the filestore as backup location otherwise it’s +possible you’ll back up the backup
  4. +
  5. Enable Use For Backups
  6. +
  7. Follow it (using the chatter) if you want to get notified when a +backup fails
  8. +
  9. To know if the backup is working correctly you can run the scheduled +action (Backup database and delete old backups) manually to test +it.
  10. +
+
+
+

Usage

+

The backup is done automatically by a scheduled action +(Backup database and delete old backups).

+
+
+

Known issues / Roadmap

+
    +
  • Configurable backup frequency: e.g. backup every 7 days in s3 and +every 4 hours on a FTP server.
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Onestein
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/fs_storage_backup/tests/__init__.py b/fs_storage_backup/tests/__init__.py new file mode 100644 index 0000000000..8b6238c52d --- /dev/null +++ b/fs_storage_backup/tests/__init__.py @@ -0,0 +1 @@ +from . import test_backup diff --git a/fs_storage_backup/tests/test_backup.py b/fs_storage_backup/tests/test_backup.py new file mode 100644 index 0000000000..c6321ac238 --- /dev/null +++ b/fs_storage_backup/tests/test_backup.py @@ -0,0 +1,57 @@ +from odoo.tests.common import TransactionCase + + +class TestBackup(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.fs_storage = cls.env.ref("fs_storage.fs_storage_demo") + cls.fs_storage.use_for_backup = True + cls.fs_storage.backup_dir = "backups" + cls.fs_storage.backup_filename_format = "backup-%(db)s-%(dt)s.%(ext)s" + cls.fs_storage.backup_keep_time = 7 + + def test_with_filestore(self): + self.fs_storage.backup_include_filestore = True + old_counts = {} + backup_locations = ( + self.env["fs.storage"].search([]).filtered(lambda r: r.use_for_backup) + ) + for fs_storage in backup_locations: + fs_storage.fs.makedirs(fs_storage.backup_dir, exist_ok=True) + old_counts[fs_storage.id] = len( + fs_storage.fs.ls(fs_storage.backup_dir, detail=False) + ) + self.env["fs.storage"].cron_backup_db() # Backup all locations + for fs_storage in backup_locations: + new_count = len(fs_storage.fs.ls(fs_storage.backup_dir, detail=False)) + self.assertEqual(old_counts[fs_storage.id] + 1, new_count) + + def test_without_filestore(self): + self.fs_storage.fs.makedirs(self.fs_storage.backup_dir, exist_ok=True) + files = self.fs_storage.fs.ls(self.fs_storage.backup_dir, detail=False) + old_count = len(list(filter(lambda f: f.endswith(".dump"), files))) + self.fs_storage.backup_include_filestore = False + self.fs_storage.backup_db() + files = self.fs_storage.fs.ls(self.fs_storage.backup_dir, detail=False) + new_count = len(list(filter(lambda f: f.endswith(".dump"), files))) + self.assertEqual(old_count + 1, new_count) + + def test_cleanup_no_dir(self): + self.fs_storage.backup_dir = "backups123" + with self.assertLogs(level="ERROR"): + self.fs_storage.cleanup_old_backups() + + def test_no_connection(self): + fs_storage = self.env["fs.storage"].create( + { + "name": "FTP", + "code": "ftp", + "protocol": "ftp", + "directory_path": ".", + "options": '{"host": "host", "port": 21}', # Non existent host + "use_for_backup": True, + } + ) + with self.assertLogs(level="ERROR"): + fs_storage.backup_db() diff --git a/fs_storage_backup/views/fs_storage_view.xml b/fs_storage_backup/views/fs_storage_view.xml new file mode 100644 index 0000000000..d0e7994bec --- /dev/null +++ b/fs_storage_backup/views/fs_storage_view.xml @@ -0,0 +1,20 @@ + + + + + fs.storage + + + + + + + + + + + + + + +