diff --git a/.oca/oca-port/blacklist/storage_backend.json b/.oca/oca-port/blacklist/storage_backend.json new file mode 100644 index 0000000000..c34693e647 --- /dev/null +++ b/.oca/oca-port/blacklist/storage_backend.json @@ -0,0 +1,9 @@ +{ + "pull_requests": { + "orphaned_commits": "false-positive", + "78": "false-positive", + "97": "false-positive", + "106": "false-positive", + "298": "false-positive" + } +} diff --git a/storage_backend/README.rst b/storage_backend/README.rst new file mode 100644 index 0000000000..aca6482536 --- /dev/null +++ b/storage_backend/README.rst @@ -0,0 +1,100 @@ +=============== +Storage Backend +=============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e7d57aec1028f8dcc4fc465d14076ef3f8d791eb5425d23fb5d3fb4ef30c565b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github + :target: https://github.com/OCA/storage/tree/18.0/storage_backend + :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-storage_backend + :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| + + + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + + + +Changelog +========= + + + +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 +------- + +* Akretion + +Contributors +------------ + +- Sébastien BEAU +- Raphaël Reverdy +- Florian da Costa +- Cédric Pigeon +- Renato Lima +- Benoît Guillot +- Laurent Mignon +- Denis Roussel +- Thien Vo + +Other credits +------------- + +The migration of this module from 16.0 to 18.0 was financially supported +by Camptocamp. + +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/storage_backend/__init__.py b/storage_backend/__init__.py new file mode 100644 index 0000000000..0f00a6730d --- /dev/null +++ b/storage_backend/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import components diff --git a/storage_backend/__manifest__.py b/storage_backend/__manifest__.py new file mode 100644 index 0000000000..3959cdbe63 --- /dev/null +++ b/storage_backend/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Storage Backend", + "summary": "Implement the concept of Storage with amazon S3, sftp...", + "version": "18.0.1.0.0", + "category": "Storage", + "website": "https://github.com/OCA/storage", + "author": " Akretion, Odoo Community Association (OCA)", + "license": "LGPL-3", + "development_status": "Production/Stable", + "installable": True, + "depends": ["base", "component", "server_environment"], + "data": [ + "views/backend_storage_view.xml", + "data/data.xml", + "security/ir.model.access.csv", + ], +} diff --git a/storage_backend/components/__init__.py b/storage_backend/components/__init__.py new file mode 100644 index 0000000000..264029fa30 --- /dev/null +++ b/storage_backend/components/__init__.py @@ -0,0 +1,2 @@ +from . import base_adapter +from . import filesystem_adapter diff --git a/storage_backend/components/base_adapter.py b/storage_backend/components/base_adapter.py new file mode 100644 index 0000000000..2ae2992cf0 --- /dev/null +++ b/storage_backend/components/base_adapter.py @@ -0,0 +1,69 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +# Copyright 2020 ACSONE SA/NV () +# @author Simone Orsi + +import os +import re + +from odoo.addons.component.core import AbstractComponent + + +class BaseStorageAdapter(AbstractComponent): + _name = "base.storage.adapter" + _collection = "storage.backend" + + def _fullpath(self, relative_path): + dp = self.collection.directory_path + if not dp or relative_path.startswith(dp): + return relative_path + return os.path.join(dp, relative_path) + + def add(self, relative_path, data, **kwargs): + raise NotImplementedError + + def get(self, relative_path, **kwargs): + raise NotImplementedError + + def list(self, relative_path=""): + raise NotImplementedError + + def find_files(self, pattern, relative_path="", **kwargs): + """Find files matching given pattern. + + :param pattern: regex expression + :param relative_path: optional relative path containing files + :return: list of file paths as full paths from the root + """ + regex = re.compile(pattern) + filelist = self.list(relative_path) + files_matching = [ + regex.match(file_).group() for file_ in filelist if regex.match(file_) + ] + filepaths = [] + if files_matching: + filepaths = [ + os.path.join(self._fullpath(relative_path) or "", filename) + for filename in files_matching + ] + return filepaths + + def move_files(self, files, destination_path, **kwargs): + """Move files to given destination. + + :param files: list of file paths to be moved + :param destination_path: directory path where to move files + :return: None + """ + raise NotImplementedError + + def delete(self, relative_path): + raise NotImplementedError + + # You can define `validate_config` on your own adapter + # to make validation button available on UI. + # This method should simply pass smoothly when validation is ok, + # otherwise it should raise an exception. + # def validate_config(self): + # raise NotImplementedError diff --git a/storage_backend/components/filesystem_adapter.py b/storage_backend/components/filesystem_adapter.py new file mode 100644 index 0000000000..8bcbc93936 --- /dev/null +++ b/storage_backend/components/filesystem_adapter.py @@ -0,0 +1,75 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging +import os +import shutil + +from odoo.exceptions import AccessError + +from odoo.addons.component.core import Component + +_logger = logging.getLogger(__name__) + + +def is_safe_path(basedir, path): + return os.path.realpath(path).startswith(basedir) + + +class FileSystemStorageBackend(Component): + _name = "filesystem.adapter" + _inherit = "base.storage.adapter" + _usage = "filesystem" + + def _basedir(self): + return os.path.join(self.env["ir.attachment"]._filestore(), "storage") + + def _fullpath(self, relative_path): + """This will build the full path for the file, we force to + store the data inside the filestore in the directory 'storage". + Becarefull if you implement your own custom path, end user + should never be able to write or read unwanted filesystem file""" + full_path = super()._fullpath(relative_path) + base_dir = self._basedir() + full_path = os.path.join(base_dir, full_path) + if not is_safe_path(base_dir, full_path): + raise AccessError(self.env._("Access to %s is forbidden") % full_path) + return full_path + + def add(self, relative_path, data, **kwargs): + full_path = self._fullpath(relative_path) + dirname = os.path.dirname(full_path) + if not os.path.isdir(dirname): + os.makedirs(dirname) + with open(full_path, "wb") as my_file: + my_file.write(data) + + def get(self, relative_path, **kwargs): + full_path = self._fullpath(relative_path) + with open(full_path, "rb") as my_file: + data = my_file.read() + return data + + def list(self, relative_path=""): + full_path = self._fullpath(relative_path) + if os.path.isdir(full_path): + return os.listdir(full_path) + return [] + + def delete(self, relative_path): + full_path = self._fullpath(relative_path) + try: + os.remove(full_path) + except FileNotFoundError: + _logger.warning("File not found in %s", full_path) + + def move_files(self, files, destination_path): + result = [] + for file_path in files: + if not os.path.exists(destination_path): + os.makedirs(destination_path) + filename = os.path.basename(file_path) + destination_file = os.path.join(destination_path, filename) + result.append(shutil.move(file_path, destination_file)) + return result diff --git a/storage_backend/data/data.xml b/storage_backend/data/data.xml new file mode 100644 index 0000000000..1b025cf1a4 --- /dev/null +++ b/storage_backend/data/data.xml @@ -0,0 +1,7 @@ + + + + Filesystem Backend + filesystem + + diff --git a/storage_backend/i18n/es.po b/storage_backend/i18n/es.po new file mode 100644 index 0000000000..9d19a97738 --- /dev/null +++ b/storage_backend/i18n/es.po @@ -0,0 +1,151 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_backend +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-10-29 00:15+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\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" +"X-Generator: Weblate 4.17\n" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/components/filesystem_adapter.py:0 +#, python-format +msgid "Access to %s is forbidden" +msgstr "El acceso a %s está prohibido" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type +msgid "Backend Type" +msgstr "Tipo de servidor" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type_env_default +msgid "Backend Type Env Default" +msgstr "Tipo de servidor Ent Por defecto" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type_env_is_editable +msgid "Backend Type Env Is Editable" +msgstr "El Tipo de Entorno del Servidor es Editable" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "¡Error en la Prueba de Conexión!" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "¡Conexión de Prueba Exitosa!" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path +msgid "Directory Path" +msgstr "Ruta del Directorio" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path_env_default +msgid "Directory Path Env Default" +msgstr "Ruta del Directorio Ent Predet" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path_env_is_editable +msgid "Directory Path Env Is Editable" +msgstr "El Entorno de Ruta del Directorio es Editable" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "¡Todo parece correctamente configurado!" + +#. module: storage_backend +#: model:ir.model.fields.selection,name:storage_backend.selection__storage_backend__backend_type__filesystem +msgid "Filesystem" +msgstr "Sistema de archivos" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__has_validation +msgid "Has Validation" +msgstr "Tiene Validación" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__id +msgid "ID" +msgstr "ID(identificación)" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend____last_update +msgid "Last Modified on" +msgstr "Última Modifiación el" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__write_uid +msgid "Last Updated by" +msgstr "Última Actualización por" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__name +msgid "Name" +msgstr "Nombre" + +#. module: storage_backend +#: model:ir.model.fields,help:storage_backend.field_storage_backend__directory_path +#: model:ir.model.fields,help:storage_backend.field_storage_backend__directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "Ruta relativa al directorio para almacenar el archivo" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__server_env_defaults +msgid "Server Env Defaults" +msgstr "Valores por defecto del Entorno de Servidor" + +#. module: storage_backend +#: model:ir.actions.act_window,name:storage_backend.act_open_storage_backend_view +#: model:ir.model,name:storage_backend.model_storage_backend +#: model:ir.ui.menu,name:storage_backend.menu_storage +#: model:ir.ui.menu,name:storage_backend.menu_storage_backend +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_form +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_search +msgid "Storage Backend" +msgstr "Servidor de Almacenamiento" + +#. module: storage_backend +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_form +msgid "Test connection" +msgstr "Probar conexión" diff --git a/storage_backend/i18n/it.po b/storage_backend/i18n/it.po new file mode 100644 index 0000000000..54bbe42040 --- /dev/null +++ b/storage_backend/i18n/it.po @@ -0,0 +1,151 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_backend +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-11-28 14:33+0000\n" +"Last-Translator: mymage \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" +"X-Generator: Weblate 4.17\n" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/components/filesystem_adapter.py:0 +#, python-format +msgid "Access to %s is forbidden" +msgstr "L'accesso a %s è vietato" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type +msgid "Backend Type" +msgstr "Tipo backend" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type_env_default +msgid "Backend Type Env Default" +msgstr "Tipo backend ambiene predefinito" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type_env_is_editable +msgid "Backend Type Env Is Editable" +msgstr "Il tipo backend ambiente è modificabile" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "Test connessione fallito!" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "Test connessione avvenuto con successo!" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path +msgid "Directory Path" +msgstr "Percorso cartella" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path_env_default +msgid "Directory Path Env Default" +msgstr "Percorso cartella ambiente predefinito" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path_env_is_editable +msgid "Directory Path Env Is Editable" +msgstr "Percorso cartella ambiente è modificabile" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "Tutto sembra impostato correttamente!" + +#. module: storage_backend +#: model:ir.model.fields.selection,name:storage_backend.selection__storage_backend__backend_type__filesystem +msgid "Filesystem" +msgstr "Filesystem" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__has_validation +msgid "Has Validation" +msgstr "Ha validazione" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__id +msgid "ID" +msgstr "ID" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__name +msgid "Name" +msgstr "Nome" + +#. module: storage_backend +#: model:ir.model.fields,help:storage_backend.field_storage_backend__directory_path +#: model:ir.model.fields,help:storage_backend.field_storage_backend__directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "Percorso relativo alla cartella per archiviare il file" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__server_env_defaults +msgid "Server Env Defaults" +msgstr "Server ambiente predefinito" + +#. module: storage_backend +#: model:ir.actions.act_window,name:storage_backend.act_open_storage_backend_view +#: model:ir.model,name:storage_backend.model_storage_backend +#: model:ir.ui.menu,name:storage_backend.menu_storage +#: model:ir.ui.menu,name:storage_backend.menu_storage_backend +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_form +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_search +msgid "Storage Backend" +msgstr "Backend deposito" + +#. module: storage_backend +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_form +msgid "Test connection" +msgstr "Prova connessione" diff --git a/storage_backend/i18n/storage_backend.pot b/storage_backend/i18n/storage_backend.pot new file mode 100644 index 0000000000..9176cb19af --- /dev/null +++ b/storage_backend/i18n/storage_backend.pot @@ -0,0 +1,148 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_backend +# +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: storage_backend +#. odoo-python +#: code:addons/storage_backend/components/filesystem_adapter.py:0 +#, python-format +msgid "Access to %s is forbidden" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type +msgid "Backend Type" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type_env_default +msgid "Backend Type Env Default" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type_env_is_editable +msgid "Backend Type Env Is Editable" +msgstr "" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__create_uid +msgid "Created by" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__create_date +msgid "Created on" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path +msgid "Directory Path" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path_env_default +msgid "Directory Path Env Default" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path_env_is_editable +msgid "Directory Path Env Is Editable" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__display_name +msgid "Display Name" +msgstr "" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields.selection,name:storage_backend.selection__storage_backend__backend_type__filesystem +msgid "Filesystem" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__has_validation +msgid "Has Validation" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__id +msgid "ID" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend____last_update +msgid "Last Modified on" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__write_date +msgid "Last Updated on" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__name +msgid "Name" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,help:storage_backend.field_storage_backend__directory_path +#: model:ir.model.fields,help:storage_backend.field_storage_backend__directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__server_env_defaults +msgid "Server Env Defaults" +msgstr "" + +#. module: storage_backend +#: model:ir.actions.act_window,name:storage_backend.act_open_storage_backend_view +#: model:ir.model,name:storage_backend.model_storage_backend +#: model:ir.ui.menu,name:storage_backend.menu_storage +#: model:ir.ui.menu,name:storage_backend.menu_storage_backend +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_form +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_search +msgid "Storage Backend" +msgstr "" + +#. module: storage_backend +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_form +msgid "Test connection" +msgstr "" diff --git a/storage_backend/models/__init__.py b/storage_backend/models/__init__.py new file mode 100644 index 0000000000..f45f402268 --- /dev/null +++ b/storage_backend/models/__init__.py @@ -0,0 +1 @@ +from . import storage_backend diff --git a/storage_backend/models/storage_backend.py b/storage_backend/models/storage_backend.py new file mode 100644 index 0000000000..2c4e5af464 --- /dev/null +++ b/storage_backend/models/storage_backend.py @@ -0,0 +1,152 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# Copyright 2019 Camptocamp SA (http://www.camptocamp.com). +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import base64 +import fnmatch +import functools +import inspect +import logging +import warnings + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +# TODO: useful for the whole OCA? +def deprecated(reason): + """Mark functions or classes as deprecated. + + Emit warning at execution. + + The @deprecated is used with a 'reason'. + + .. code-block:: python + + @deprecated("please, use another function") + def old_function(x, y): + pass + """ + + def decorator(func1): + if inspect.isclass(func1): + fmt1 = "Call to deprecated class {name} ({reason})." + else: + fmt1 = "Call to deprecated function {name} ({reason})." + + @functools.wraps(func1) + def new_func1(*args, **kwargs): + warnings.simplefilter("always", DeprecationWarning) + warnings.warn( + fmt1.format(name=func1.__name__, reason=reason), + category=DeprecationWarning, + stacklevel=2, + ) + warnings.simplefilter("default", DeprecationWarning) + return func1(*args, **kwargs) + + return new_func1 + + return decorator + + +class StorageBackend(models.Model): + _name = "storage.backend" + _inherit = ["collection.base", "server.env.mixin"] + _backend_name = "storage_backend" + _description = "Storage Backend" + + name = fields.Char(required=True) + backend_type = fields.Selection( + selection=[("filesystem", "Filesystem")], required=True, default="filesystem" + ) + directory_path = fields.Char( + help="Relative path to the directory to store the file" + ) + has_validation = fields.Boolean(compute="_compute_has_validation") + + def _compute_has_validation(self): + for rec in self: + if not rec.backend_type: + # with server_env + # this code can be triggered + # before a backend_type has been set + # get_adapter() can't work without backend_type + rec.has_validation = False + continue + adapter = rec._get_adapter() + rec.has_validation = hasattr(adapter, "validate_config") + + @property + def _server_env_fields(self): + return {"backend_type": {}, "directory_path": {}} + + def add(self, relative_path, data, binary=True, **kwargs): + if not binary: + data = base64.b64decode(data) + return self._forward("add", relative_path, data, **kwargs) + + def get(self, relative_path, binary=True, **kwargs): + data = self._forward("get", relative_path, **kwargs) + if not binary and data: + data = base64.b64encode(data) + return data + + def list_files(self, relative_path="", pattern=False): + names = self._forward("list", relative_path) + if pattern: + names = fnmatch.filter(names, pattern) + return names + + def find_files(self, pattern, relative_path="", **kw): + return self._forward("find_files", pattern, relative_path=relative_path) + + def move_files(self, files, destination_path, **kw): + return self._forward("move_files", files, destination_path, **kw) + + def delete(self, relative_path): + return self._forward("delete", relative_path) + + def _forward(self, method, *args, **kwargs): + _logger.debug( + "Backend Storage ID: %s type %s: %s file %s %s", + self.backend_type, + self.id, + method, + args, + kwargs, + ) + self.ensure_one() + adapter = self._get_adapter() + return getattr(adapter, method)(*args, **kwargs) + + def _get_adapter(self): + with self.work_on(self._name) as work: + return work.component(usage=self.backend_type) + + def action_test_config(self): + if not self.has_validation: + raise AttributeError("Validation not supported!") + adapter = self._get_adapter() + try: + adapter.validate_config() + title = self.env._("Connection Test Succeeded!") + message = self.env._("Everything seems properly set up!") + msg_type = "success" + except Exception as err: + title = self.env._("Connection Test Failed!") + message = str(err) + msg_type = "danger" + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": title, + "message": message, + "type": msg_type, + "sticky": False, + }, + } diff --git a/storage_backend/pyproject.toml b/storage_backend/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/storage_backend/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/storage_backend/readme/CONTRIBUTORS.md b/storage_backend/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..3536a8c91b --- /dev/null +++ b/storage_backend/readme/CONTRIBUTORS.md @@ -0,0 +1,9 @@ +- Sébastien BEAU \<\> +- Raphaël Reverdy \<\> +- Florian da Costa \<\> +- Cédric Pigeon \<\> +- Renato Lima \<\> +- Benoît Guillot \<\> +- Laurent Mignon \<\> +- Denis Roussel \<\> +- Thien Vo \<\> diff --git a/storage_backend/readme/CREDITS.md b/storage_backend/readme/CREDITS.md new file mode 100644 index 0000000000..57e03a9fe7 --- /dev/null +++ b/storage_backend/readme/CREDITS.md @@ -0,0 +1 @@ +The migration of this module from 16.0 to 18.0 was financially supported by Camptocamp. diff --git a/storage_backend/readme/DESCRIPTION.md b/storage_backend/readme/DESCRIPTION.md new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/storage_backend/readme/DESCRIPTION.md @@ -0,0 +1 @@ + diff --git a/storage_backend/readme/HISTORY.md b/storage_backend/readme/HISTORY.md new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/storage_backend/readme/HISTORY.md @@ -0,0 +1 @@ + diff --git a/storage_backend/readme/USAGE.md b/storage_backend/readme/USAGE.md new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/storage_backend/readme/USAGE.md @@ -0,0 +1 @@ + diff --git a/storage_backend/security/ir.model.access.csv b/storage_backend/security/ir.model.access.csv new file mode 100644 index 0000000000..dd245d4814 --- /dev/null +++ b/storage_backend/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_storage_backend_edit,storage_backend edit,model_storage_backend,base.group_system,1,1,1,1 diff --git a/storage_backend/static/description/icon.png b/storage_backend/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/storage_backend/static/description/icon.png differ diff --git a/storage_backend/static/description/index.html b/storage_backend/static/description/index.html new file mode 100644 index 0000000000..1e49326e72 --- /dev/null +++ b/storage_backend/static/description/index.html @@ -0,0 +1,444 @@ + + + + + +Storage Backend + + + +
+

Storage Backend

+ + +

Production/Stable License: LGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

Table of contents

+ +
+

Usage

+
+ +
+

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

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The migration of this module from 16.0 to 18.0 was financially supported +by Camptocamp.

+
+
+

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/storage_backend/tests/__init__.py b/storage_backend/tests/__init__.py new file mode 100644 index 0000000000..2161dcab39 --- /dev/null +++ b/storage_backend/tests/__init__.py @@ -0,0 +1,2 @@ +from . import common +from . import test_filesystem diff --git a/storage_backend/tests/common.py b/storage_backend/tests/common.py new file mode 100644 index 0000000000..3f25f6db7e --- /dev/null +++ b/storage_backend/tests/common.py @@ -0,0 +1,78 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import base64 +from unittest import mock + +from odoo.addons.component.tests.common import TransactionComponentCase + + +class BackendStorageTestMixin: + def _test_setting_and_getting_data(self): + # Check that the directory is empty + files = self.backend.list_files() + self.assertNotIn(self.filename, files) + + # Add a new file + self.backend.add( + self.filename, self.filedata, mimetype="text/plain", binary=False + ) + + # Check that the file exist + files = self.backend.list_files() + self.assertIn(self.filename, files) + + # Retrieve the file added + data = self.backend.get(self.filename, binary=False) + self.assertEqual(data, self.filedata) + + # Delete the file + self.backend.delete(self.filename) + files = self.backend.list_files() + self.assertNotIn(self.filename, files) + + def _test_setting_and_getting_data_from_root(self): + self._test_setting_and_getting_data() + + def _test_setting_and_getting_data_from_dir(self): + self.backend.directory_path = self.case_with_subdirectory + self._test_setting_and_getting_data() + + def _test_find_files( + self, + backend, + adapter_dotted_path, + mocked_filepaths, + pattern, + expected_filepaths, + ): + with mock.patch(adapter_dotted_path + ".list") as mocked: + mocked.return_value = mocked_filepaths + res = backend.find_files(pattern) + self.assertEqual(sorted(res), sorted(expected_filepaths)) + + def _test_move_files( + self, + backend, + adapter_dotted_path, + filename, + destination_path, + expected_filepaths, + ): + with mock.patch(adapter_dotted_path + ".move_files") as mocked: + mocked.return_value = expected_filepaths + res = backend.move_files(filename, destination_path) + self.assertEqual(sorted(res), sorted(expected_filepaths)) + + +class CommonCase(TransactionComponentCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.backend = cls.env.ref("storage_backend.default_storage_backend") + cls.filedata = base64.b64encode(b"This is a simple file") + cls.filename = "test_file.txt" + cls.case_with_subdirectory = "subdirectory/here" + cls.demo_user = cls.env.ref("base.user_demo") diff --git a/storage_backend/tests/test_filesystem.py b/storage_backend/tests/test_filesystem.py new file mode 100644 index 0000000000..a136401b28 --- /dev/null +++ b/storage_backend/tests/test_filesystem.py @@ -0,0 +1,65 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import os + +from odoo.exceptions import AccessError + +from .common import BackendStorageTestMixin, CommonCase + +ADAPTER_PATH = ( + "odoo.addons.storage_backend.components.filesystem_adapter.FileSystemStorageBackend" +) + + +class FileSystemCase(CommonCase, BackendStorageTestMixin): + def test_setting_and_getting_data_from_root(self): + self._test_setting_and_getting_data_from_root() + + def test_setting_and_getting_data_from_dir(self): + self._test_setting_and_getting_data_from_dir() + + def test_find_files(self): + good_filepaths = ["somepath/file%d.good" % x for x in range(1, 10)] + bad_filepaths = ["somepath/file%d.bad" % x for x in range(1, 10)] + mocked_filepaths = bad_filepaths + good_filepaths + backend = self.backend.sudo() + base_dir = backend._get_adapter()._basedir() + expected = [base_dir + "/" + path for path in good_filepaths] + self._test_find_files( + backend, ADAPTER_PATH, mocked_filepaths, r".*\.good$", expected + ) + + def test_move_files(self): + backend = self.backend.sudo() + base_dir = backend._get_adapter()._basedir() + expected = [base_dir + "/" + self.filename] + destination_path = os.path.join(base_dir, "destination") + self._test_move_files( + backend, ADAPTER_PATH, self.filename, destination_path, expected + ) + + +class FileSystemDemoUserAccessCase(CommonCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.backend = cls.backend.with_user(cls.demo_user) + + def test_cannot_add_file(self): + with self.assertRaises(AccessError): + self.backend.add( + self.filename, self.filedata, mimetype="text/plain", binary=False + ) + + def test_cannot_list_file(self): + with self.assertRaises(AccessError): + self.backend.list_files() + + def test_cannot_read_file(self): + with self.assertRaises(AccessError): + self.backend.get(self.filename, binary=False) + + def test_cannot_delete_file(self): + with self.assertRaises(AccessError): + self.backend.delete(self.filename) diff --git a/storage_backend/views/backend_storage_view.xml b/storage_backend/views/backend_storage_view.xml new file mode 100644 index 0000000000..5c9735111a --- /dev/null +++ b/storage_backend/views/backend_storage_view.xml @@ -0,0 +1,81 @@ + + + + storage.backend + + + + + + + + + storage.backend + +
+
+ +
+ +
+
+ + + + +
+
+
+
+ + storage.backend + + + + + + + + Storage Backend + ir.actions.act_window + storage.backend + list,form + + [] + {} + + + + + form + + + + + + list + + + + +