diff --git a/fs_storage/README.rst b/fs_storage/README.rst new file mode 100644 index 0000000000..372ec4d1b6 --- /dev/null +++ b/fs_storage/README.rst @@ -0,0 +1,301 @@ +========================== +Filesystem Storage Backend +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6e7abac468ff2942178ac2df68774f75820b618173acf8c7b76f51fbdbe3b7bc + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-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/fs_storage + :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 + :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| + +This addon is a technical addon that allows you to define filesystem +like storage for your data. It's used by other addons to store their +data in a transparent way into different kind of storages. + +Through the fs.storage record, you get access to an object that +implements the +`fsspec.spec.AbstractFileSystem `__ +interface and therefore give you an unified interface to access your +data whatever the storage protocol you decide to use. + +The list of supported protocols depends on the installed fsspec +implementations. By default, the addon will install the following +protocols: + +- LocalFileSystem +- MemoryFileSystem +- ZipFileSystem +- TarFileSystem +- FTPFileSystem +- CachingFileSystem +- WholeFileSystem +- SimplCacheFileSystem +- ReferenceFileSystem +- GenericFileSystem +- DirFileSystem +- DatabricksFileSystem +- GitHubFileSystem +- JupiterFileSystem +- OdooFileSystem + +The OdooFileSystem is the one that allows you to store your data into a +directory mounted into your Odoo's storage directory. This is the +default FS Storage when creating a new fs.storage record. + +Others protocols are available through the installation of additional +python packages: + +- DropboxDriveFileSystem -> pip install fsspec[dropbox] +- HTTPFileSystem -> pip install fsspec[http] +- HTTPSFileSystem -> pip install fsspec[http] +- GCSFileSystem -> pip install fsspec[gcs] +- GSFileSystem -> pip install fsspec[gs] +- GoogleDriveFileSystem -> pip install gdrivefs +- SFTPFileSystem -> pip install fsspec[sftp] +- HaddoopFileSystem -> pip install fsspec[hdfs] +- S3FileSystem -> pip install fsspec[s3] +- WandbFS -> pip install wandbfs +- OCIFileSystem -> pip install fsspec[oci] +- AsyncLocalFileSystem -> pip install 'morefs[asynclocalfs] +- AzureDatalakeFileSystem -> pip install fsspec[adl] +- AzureBlobFileSystem -> pip install fsspec[abfs] +- DaskWorkerFileSystem -> pip install fsspec[dask] +- GitFileSystem -> pip install fsspec[git] +- SMBFileSystem -> pip install fsspec[smb] +- LibArchiveFileSystem -> pip install fsspec[libarchive] +- OSSFileSystem -> pip install ossfs +- WebdavFileSystem -> pip install webdav4 +- DVCFileSystem -> pip install dvc +- XRootDFileSystem -> pip install fsspec-xrootd + +This list of supported protocols is not exhaustive or could change in +the future depending on the fsspec releases. You can find more +information about the supported protocols on the `fsspec +documentation `__. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Configuration +------------- + +When you create a new backend, you must specify the following: + +- The name of the backend. This is the name that will be used to + identify the backend into Odoo + +- The code of the backend. This code will identify the backend into the + store_fname field of the ir.attachment model. This code must be + unique. It will be used as scheme. example of the store_fname field: + ``odoofs://abs34Tg11``. + +- The protocol used by the backend. The protocol refers to the + supported protocols of the fsspec python package. + +- A directory path. This is a root directory from which the filesystem + will be mounted. This directory must exist. + +- The protocol options. These are the options that will be passed to + the fsspec python package when creating the filesystem. These options + depend on the protocol used and are described in the fsspec + documentation. + +- Resolve env vars. This options resolves the protocol options values + starting with $ from environment variables + +- Check Connection Method. If set, Odoo will always check the + connection before using a storage and it will remove the fs + connection from the cache if the check fails. + + - ``Create Marker file``: create a hidden file on remote and then + check it exists with Use it if you have write access to the remote + and if it is not an issue to leave the marker file in the root + directory. + - ``List file``: list all files from the root directory. You can use + it if the directory path does not contain a big list of files (for + performance reasons) + +Some protocols defined in the fsspec package are wrappers around other +protocols. For example, the SimpleCacheFileSystem protocol is a wrapper +around any local filesystem protocol. In such cases, you must specify +into the protocol options the protocol to be wrapped and the options to +be passed to the wrapped protocol. + +For example, if you want to create a backend that uses the +SimpleCacheFileSystem protocol, after selecting the +SimpleCacheFileSystem protocol, you must specify the protocol options as +follows: + +.. code:: python + + { + "directory_path": "/tmp/my_backend", + "target_protocol": "odoofs", + "target_options": {...}, + } + +In this example, the SimpleCacheFileSystem protocol will be used as a +wrapper around the odoofs protocol. + +Server Environment +------------------ + +To ease the management of the filesystem storages configuration accross +the different environments, the configuration of the filesystem storages +can be defined in environment files or directly in the main +configuration file. For example, the configuration of a filesystem +storage with the code fsprod can be provided in the main configuration +file as follows: + +.. code:: ini + + [fs_storage.fsprod] + protocol=s3 + options={"endpoint_url": "https://my_s3_server/", "key": "KEY", "secret": "SECRET"} + directory_path=my_bucket + +To work, a storage.backend record must exist with the code fsprod into +the database. In your configuration section, you can specify the value +for the following fields: + +- protocol +- options +- directory_path + +Migration from storage_backend +------------------------------ + +The fs_storage addon can be used to replace the storage_backend addon. +(It has been designed to be a drop-in replacement for the +storage_backend addon). To ease the migration, the fs.storage model +defines the high-level methods available in the storage_backend model. +These methods are: + +- add +- get +- list_files +- find_files +- move_files +- delete + +These methods are wrappers around the methods of the +fsspec.AbstractFileSystem class (see +https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.spec.AbstractFileSystem). +These methods are marked as deprecated and will be removed in a future +version (V18) of the addon. You should use the methods of the +fsspec.AbstractFileSystem class instead since they are more flexible and +powerful. You can access the instance of the fsspec.AbstractFileSystem +class using the fs property of a fs.storage record. + +Known issues / Roadmap +====================== + +- Transactions: fsspec comes with a transactional mechanism that once + started, gathers all the files created during the transaction, and if + the transaction is committed, moves them to their final locations. It + would be useful to bridge this with the transactional mechanism of + odoo. This would allow to ensure that all the files created during a + transaction are either all moved to their final locations, or all + deleted if the transaction is rolled back. This mechanism is only + valid for files created during the transaction by a call to the open + method of the file system. It is not valid for others operations, + such as rm, mv_file, ... . + +Changelog +========= + +16.0.1.1.0 (2023-12-22) +----------------------- + +**Features** + +- Add parameter on storage backend to resolve protocol options values + starting with $ from environment variables + (`#303 `__) + +16.0.1.0.3 (2023-10-17) +----------------------- + +**Bugfixes** + +- Fix access to technical models to be able to upload attachments for + users with basic access + (`#289 `__) + +16.0.1.0.2 (2023-10-09) +----------------------- + +**Bugfixes** + +- Avoid config error when using the webdav protocol. The auth option is + expected to be a tuple not a list. Since our config is loaded from a + json file, we cannot use tuples. The fix converts the list to a tuple + when the config is related to a webdav protocol and the auth option + is into the confix. + (`#285 `__) + +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 +------- + +* ACSONE SA/NV + +Contributors +------------ + +- Laurent Mignon +- Sébastien BEAU + +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/__init__.py b/fs_storage/__init__.py new file mode 100644 index 0000000000..6f3a6c7170 --- /dev/null +++ b/fs_storage/__init__.py @@ -0,0 +1,7 @@ +# register protocols first +from . import odoo_file_system +from . import rooted_dir_file_system + +# then add normal imports +from . import models +from . import wizards diff --git a/fs_storage/__manifest__.py b/fs_storage/__manifest__.py new file mode 100644 index 0000000000..87b7d3c5a3 --- /dev/null +++ b/fs_storage/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Filesystem Storage Backend", + "summary": "Implement the concept of Storage with amazon S3, sftp...", + "version": "18.0.1.0.0", + "category": "FS Storage", + "website": "https://github.com/OCA/storage", + "author": " ACSONE SA/NV, Odoo Community Association (OCA)", + "license": "LGPL-3", + "development_status": "Beta", + "installable": True, + "depends": ["base", "base_sparse_field", "server_environment"], + "data": [ + "views/fs_storage_view.xml", + "security/ir.model.access.csv", + "wizards/fs_test_connection.xml", + ], + "demo": ["demo/fs_storage_demo.xml"], + "external_dependencies": {"python": ["fsspec>=2024.5.0"]}, +} diff --git a/fs_storage/demo/fs_storage_demo.xml b/fs_storage/demo/fs_storage_demo.xml new file mode 100644 index 0000000000..0917d06221 --- /dev/null +++ b/fs_storage/demo/fs_storage_demo.xml @@ -0,0 +1,8 @@ + + + + Odoo Filesystem Backend + odoofs + odoofs + + diff --git a/fs_storage/i18n/es.po b/fs_storage/i18n/es.po new file mode 100644 index 0000000000..f558b305aa --- /dev/null +++ b/fs_storage/i18n/es.po @@ -0,0 +1,297 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-04-24 10:40+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: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Available options" +msgstr "Opciones disponibles" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_properties +msgid "Available properties" +msgstr "Propiedades disponibles" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__code +msgid "Code" +msgstr "Código" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "¡Conexión de prueba fallida!" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "¡Conexión de prueba exitosa!" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_protocol +msgid "Describes Protocol" +msgstr "Descripción del Protocolo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path +msgid "Directory Path" +msgstr "Ruta del Directorio" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_directory_path_env_default +msgid "Directory Path Env Default" +msgstr "Ruta del Directorio Env Predet" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Enter you fsspec options here." +msgstr "Introduzca aquí sus opciones fsspec." + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "¡Todo parece correctamente configurado!" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_storage_view +#: model:ir.model,name:fs_storage.model_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_search_view +msgid "FS Storage" +msgstr "Almacenamiento FS" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__json_options +msgid "Json Options" +msgstr "Opciones Json" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_uid +msgid "Last Updated by" +msgstr "Actualizado por Última vez por" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__name +msgid "Name" +msgstr "Nombre" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options +msgid "Options" +msgstr "Opciones" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_options_env_default +msgid "Options Env Default" +msgstr "Opciones Env Por Defecto" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol +msgid "Protocol" +msgstr "Protocolo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_descr +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Protocol Descr" +msgstr "Descr. del Protocolo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_protocol_env_default +msgid "Protocol Env Default" +msgstr "Protocolo Env Predeterminado" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path +#: model:ir.model.fields,help:fs_storage.field_fs_storage__x_directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "Ruta relativa al directorio para almacenar el archivo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env +msgid "Resolve env vars" +msgstr "Resolver las variables de entorno" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__eval_options_from_env +msgid "" +"Resolve options values starting with $ from environment variables. e.g\n" +" {\n" +" \"endpoint_url\": \"$AWS_ENDPOINT_URL\",\n" +" }\n" +" " +msgstr "" +"Resuelva los valores de opciones que comienzan con $ a partir de variables " +"de entorno. p.ej\n" +" {\n" +" \"endpoint_url\": \"$AWS_ENDPOINT_URL\",\n" +" }\n" +" " + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__server_env_defaults +msgid "Server Env Defaults" +msgstr "Valores por defecto del Entorno de Servidor" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__code +msgid "" +"Technical code used to identify the storage backend into the code.This code " +"must be unique. This code is used for example to define the storage backend " +"to store the attachments via the configuration parameter 'ir_attachment." +"storage.force.database' when the module 'fs_attachment' is installed." +msgstr "" +"Código técnico utilizado para identificar el servidor de almacenamiento en " +"el código. Este código debe ser único. Este código se utiliza, por ejemplo, " +"para definir el servidor de almacenamiento para guardar los archivos " +"adjuntos mediante el parámetro de configuración \"ir_attachment.storage." +"force.database\" cuando se instala el módulo \"fs_attachment\"." + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Test connection" +msgstr "Probar conexión" + +#. module: fs_storage +#: model:ir.model.constraint,message:fs_storage.constraint_fs_storage_code_uniq +msgid "The code must be unique" +msgstr "El código tiene que ser único" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "The options must be a valid JSON" +msgstr "Las opciones tienen que estar definidas en un JSON válido" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__json_options +msgid "The options used to initialize the filesystem.\n" +msgstr "Las opciones utilizadas para inicializar el sistema de archivos.\n" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options +#: model:ir.model.fields,help:fs_storage.field_fs_storage__x_options_env_default +msgid "" +"The options used to initialize the filesystem.\n" +"This is a JSON field that depends on the protocol used.\n" +"For example, for the sftp protocol, you can provide the following:\n" +"{\n" +" 'host': 'my.sftp.server',\n" +" 'ssh_kwrags': {\n" +" 'username': 'myuser',\n" +" 'password': 'mypassword',\n" +" 'port': 22,\n" +" }\n" +"}\n" +"For more information, please refer to the fsspec documentation:\n" +"https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-" +"implementations" +msgstr "" +"Las opciones utilizadas para inicializar el sistema de ficheros.\n" +"Este es un campo JSON que depende del protocolo utilizado.\n" +"Por ejemplo, para el protocolo sftp, puede proporcionar lo siguiente:\n" +"{\n" +" 'host': 'mi.sftp.server',\n" +" 'ssh_kwrags': {\n" +" 'username': 'myuser',\n" +" 'password': 'mypassword',\n" +" ' port': 22,\n" +" }\n" +"}\n" +"Para más información, consulta la documentación de fsspec:\n" +"https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-" +"implementations" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options_protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__x_protocol_env_default +msgid "" +"The protocol used to access the content of filesystem.\n" +"This list is the one supported by the fsspec library (see https://filesystem-" +"spec.readthedocs.io/en/latest). A filesystem protocolis added by default and " +"refers to the odoo local filesystem.\n" +"Pay attention that according to the protocol, some options must beprovided " +"through the options field." +msgstr "" +"El protocolo utilizado para acceder al contenido del sistema de ficheros.\n" +"Esta lista es la soportada por la librería fsspec (ver https://filesystem-" +"spec.readthedocs.io/en/latest). Un protocolo de sistema de archivos es " +"agregado por defecto y se refiere al sistema de archivos local de odoo.\n" +"Preste atención que de acuerdo al protocolo, algunas opciones deben ser " +"provistas a través del campo opciones." + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_directory_path_env_is_editable +msgid "X Directory Path Env Is Editable" +msgstr "X La Ruta de Directorio Env Es Editable" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_options_env_is_editable +msgid "X Options Env Is Editable" +msgstr "X Las Opciones Env son Editables" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_protocol_env_is_editable +msgid "X Protocol Env Is Editable" +msgstr "El Protocolo X Env es Editable" + +#~ msgid "Directory Path Env Is Editable" +#~ msgstr "La Ruta de Directorio Env es Editable" + +#~ msgid "Last Modified on" +#~ msgstr "Última Modificación el" + +#~ msgid "Options Env Is Editable" +#~ msgstr "Las Opciones Env son Editables" + +#~ msgid "Protocol Env Is Editable" +#~ msgstr "El Protocolo Env es Editable" diff --git a/fs_storage/i18n/fs_storage.pot b/fs_storage/i18n/fs_storage.pot new file mode 100644 index 0000000000..0cddd25360 --- /dev/null +++ b/fs_storage/i18n/fs_storage.pot @@ -0,0 +1,248 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.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 +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Available options" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_properties +msgid "Available properties" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__code +msgid "Code" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_uid +msgid "Created by" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_date +msgid "Created on" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_protocol +msgid "Describes Protocol" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path +msgid "Directory Path" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_directory_path_env_default +msgid "Directory Path Env Default" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__display_name +msgid "Display Name" +msgstr "" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Enter you fsspec options here." +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_storage_view +#: model:ir.model,name:fs_storage.model_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_search_view +msgid "FS Storage" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__id +msgid "ID" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__json_options +msgid "Json Options" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__name +msgid "Name" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options +msgid "Options" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_options_env_default +msgid "Options Env Default" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol +msgid "Protocol" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_descr +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Protocol Descr" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_protocol_env_default +msgid "Protocol Env Default" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path +#: model:ir.model.fields,help:fs_storage.field_fs_storage__x_directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env +msgid "Resolve env vars" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__eval_options_from_env +msgid "" +"Resolve options values starting with $ from environment variables. e.g\n" +" {\n" +" \"endpoint_url\": \"$AWS_ENDPOINT_URL\",\n" +" }\n" +" " +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__server_env_defaults +msgid "Server Env Defaults" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__code +msgid "" +"Technical code used to identify the storage backend into the code.This code " +"must be unique. This code is used for example to define the storage backend " +"to store the attachments via the configuration parameter " +"'ir_attachment.storage.force.database' when the module 'fs_attachment' is " +"installed." +msgstr "" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Test connection" +msgstr "" + +#. module: fs_storage +#: model:ir.model.constraint,message:fs_storage.constraint_fs_storage_code_uniq +msgid "The code must be unique" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "The options must be a valid JSON" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__json_options +msgid "The options used to initialize the filesystem.\n" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options +#: model:ir.model.fields,help:fs_storage.field_fs_storage__x_options_env_default +msgid "" +"The options used to initialize the filesystem.\n" +"This is a JSON field that depends on the protocol used.\n" +"For example, for the sftp protocol, you can provide the following:\n" +"{\n" +" 'host': 'my.sftp.server',\n" +" 'ssh_kwrags': {\n" +" 'username': 'myuser',\n" +" 'password': 'mypassword',\n" +" 'port': 22,\n" +" }\n" +"}\n" +"For more information, please refer to the fsspec documentation:\n" +"https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options_protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__x_protocol_env_default +msgid "" +"The protocol used to access the content of filesystem.\n" +"This list is the one supported by the fsspec library (see https://filesystem-spec.readthedocs.io/en/latest). A filesystem protocolis added by default and refers to the odoo local filesystem.\n" +"Pay attention that according to the protocol, some options must beprovided through the options field." +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_directory_path_env_is_editable +msgid "X Directory Path Env Is Editable" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_options_env_is_editable +msgid "X Options Env Is Editable" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_protocol_env_is_editable +msgid "X Protocol Env Is Editable" +msgstr "" diff --git a/fs_storage/i18n/it.po b/fs_storage/i18n/it.po new file mode 100644 index 0000000000..86beb594d7 --- /dev/null +++ b/fs_storage/i18n/it.po @@ -0,0 +1,296 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-06-14 16:37+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: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Available options" +msgstr "Opzioni disponibili" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_properties +msgid "Available properties" +msgstr "Proprietà disponibili" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__code +msgid "Code" +msgstr "Codice" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "Test connessione fallito!" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "Test connessione avvenuto con successo!" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_protocol +msgid "Describes Protocol" +msgstr "Descrive protocollo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path +msgid "Directory Path" +msgstr "Percorso cartella" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_directory_path_env_default +msgid "Directory Path Env Default" +msgstr "Percorso cartella ambiente predefinito" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Enter you fsspec options here." +msgstr "Inserire qui le opzioni FSspec." + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "Tutto sembra impostato correttamente!" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_storage_view +#: model:ir.model,name:fs_storage.model_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_search_view +msgid "FS Storage" +msgstr "Deposito FS" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__id +msgid "ID" +msgstr "ID" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__json_options +msgid "Json Options" +msgstr "Opzioni JSON" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__name +msgid "Name" +msgstr "Nome" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options +msgid "Options" +msgstr "Opzioni" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_options_env_default +msgid "Options Env Default" +msgstr "Opzioni ambiente predefinite" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol +msgid "Protocol" +msgstr "Protcollo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_descr +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Protocol Descr" +msgstr "Descrizione protocollo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_protocol_env_default +msgid "Protocol Env Default" +msgstr "Protocollo ambiene predefinito" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path +#: model:ir.model.fields,help:fs_storage.field_fs_storage__x_directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "Percorso relativo alla cartella per archiviare il file" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env +msgid "Resolve env vars" +msgstr "Risole variabili ambiente" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__eval_options_from_env +msgid "" +"Resolve options values starting with $ from environment variables. e.g\n" +" {\n" +" \"endpoint_url\": \"$AWS_ENDPOINT_URL\",\n" +" }\n" +" " +msgstr "" +"Risolve valori opzioni iniziando con $ dalle variabili ambiente, es.\n" +" {\n" +" \"endpoint_url\": \"$AWS_ENDPOINT_URL\",\n" +" }\n" +" " + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__server_env_defaults +msgid "Server Env Defaults" +msgstr "Server ambiente predefinito" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__code +msgid "" +"Technical code used to identify the storage backend into the code.This code " +"must be unique. This code is used for example to define the storage backend " +"to store the attachments via the configuration parameter 'ir_attachment." +"storage.force.database' when the module 'fs_attachment' is installed." +msgstr "" +"Codice tecnico usato per identificare il backend deposito nel codice. Questo " +"codice deve essere univoco. Questo codice è utilizzato per esempio per " +"definire il backend deposito dove depositare gli allegati attraverso il " +"parametro configurazione 'ir_attachment.storage.force.database' quando il " +"modulo 'fs_attachment' è installato." + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Test connection" +msgstr "Prova connessione" + +#. module: fs_storage +#: model:ir.model.constraint,message:fs_storage.constraint_fs_storage_code_uniq +msgid "The code must be unique" +msgstr "Il codice deve essere univoco" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "The options must be a valid JSON" +msgstr "L'opzione deve essere un JSON valido" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__json_options +msgid "The options used to initialize the filesystem.\n" +msgstr "Le opzioni per inizializzare il filesystem.\n" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options +#: model:ir.model.fields,help:fs_storage.field_fs_storage__x_options_env_default +msgid "" +"The options used to initialize the filesystem.\n" +"This is a JSON field that depends on the protocol used.\n" +"For example, for the sftp protocol, you can provide the following:\n" +"{\n" +" 'host': 'my.sftp.server',\n" +" 'ssh_kwrags': {\n" +" 'username': 'myuser',\n" +" 'password': 'mypassword',\n" +" 'port': 22,\n" +" }\n" +"}\n" +"For more information, please refer to the fsspec documentation:\n" +"https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-" +"implementations" +msgstr "" +"Le opzioni utilizzate per inizializzare il filesystem.\n" +"Questo è uncampo JSON che dipende dal protocollo utilizzato.\n" +"Per esempio, per il protocollo SFTP, si può fornire il seguente:\n" +"{\n" +" 'host': 'my.sftp.server',\n" +" 'ssh_kwrags': {\n" +" 'username': 'myuser',\n" +" 'password': 'mypassword',\n" +" 'port': 22,\n" +" }\n" +"}\n" +"Per ulteriori informazioni, fare riferimento alla documentazione FSspec:\n" +"https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-" +"implementations" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options_protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__x_protocol_env_default +msgid "" +"The protocol used to access the content of filesystem.\n" +"This list is the one supported by the fsspec library (see https://filesystem-" +"spec.readthedocs.io/en/latest). A filesystem protocolis added by default and " +"refers to the odoo local filesystem.\n" +"Pay attention that according to the protocol, some options must beprovided " +"through the options field." +msgstr "" +"Il protocollo è utilizzato per accedere al contenuto del filesystem.\n" +"Questo elenco è quello supportato dalla libreria FSspec (vedere https://" +"filesystem-spec.readthedocs.io/en/latest). Un protocollo filesystem è " +"aggiunto in modo predefinito e fa riferimento al filesystem locale Odoo.\n" +"Fare attenzione che in accordo con il protocollo, alcune opzioni devono " +"essere fonrite attraverso il campo opzioni." + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_directory_path_env_is_editable +msgid "X Directory Path Env Is Editable" +msgstr "Il percorso ambiente cartella X è modificabile" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_options_env_is_editable +msgid "X Options Env Is Editable" +msgstr "Opzioni X ambiente sono modificabili" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_protocol_env_is_editable +msgid "X Protocol Env Is Editable" +msgstr "Protocollo X ambiente è modificabile" + +#~ msgid "Directory Path Env Is Editable" +#~ msgstr "Percorso cartella ambiente è modificabile" + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" + +#~ msgid "Options Env Is Editable" +#~ msgstr "Opzioni ambiente sono modificabili" + +#~ msgid "Protocol Env Is Editable" +#~ msgstr "Protocollo ambiente è modificabile" diff --git a/fs_storage/i18n/storage_backend.pot b/fs_storage/i18n/storage_backend.pot new file mode 100644 index 0000000000..45503812f9 --- /dev/null +++ b/fs_storage/i18n/storage_backend.pot @@ -0,0 +1,144 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage +# +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 +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__backend_type_env_default +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path_env_default +msgid " Env Default" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/components/filesystem_adapter.py:0 +#, python-format +msgid "Access to %s is forbidden" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__backend_type +msgid "Backend Type" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__backend_type_env_is_editable +msgid "Backend Type Env Is Editable" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_uid +msgid "Created by" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_date +msgid "Created on" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path +msgid "Directory Path" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path_env_is_editable +msgid "Directory Path Env Is Editable" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__display_name +msgid "Display Name" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields.selection,name:fs_storage.selection__fs_storage__backend_type__filesystem +msgid "Filesystem" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__has_validation +msgid "Has Validation" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__id +msgid "ID" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__name +msgid "Name" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__server_env_defaults +msgid "Server Env Defaults" +msgstr "" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_storage_view +#: model:ir.model,name:fs_storage.model_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_storage +#: model:ir.ui.menu,name:fs_storage.menu_fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_view_form +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_view_search +msgid "FS Storage" +msgstr "" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_view_form +msgid "Test connection" +msgstr "" diff --git a/fs_storage/i18n/zh_CN.po b/fs_storage/i18n/zh_CN.po new file mode 100644 index 0000000000..612f80dc59 --- /dev/null +++ b/fs_storage/i18n/zh_CN.po @@ -0,0 +1,277 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-08-26 08:06+0000\n" +"Last-Translator: xtanuiha \n" +"Language-Team: none\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Available options" +msgstr "可用选项" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_properties +msgid "Available properties" +msgstr "可用属性" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__code +msgid "Code" +msgstr "代码" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "连接测试失败!" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "连接测试成功!" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_uid +msgid "Created by" +msgstr "创建者" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_date +msgid "Created on" +msgstr "创建于" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_protocol +msgid "Describes Protocol" +msgstr "描述协议" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path +msgid "Directory Path" +msgstr "目录路径" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_directory_path_env_default +msgid "Directory Path Env Default" +msgstr "目录路径环境默认值" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__display_name +msgid "Display Name" +msgstr "目录路径环境默认值" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Enter you fsspec options here." +msgstr "在这里输入你的fsspec选项。" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "一切看起来都设置得当!" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_storage_view +#: model:ir.model,name:fs_storage.model_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_search_view +msgid "FS Storage" +msgstr "文件系统存储" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__id +msgid "ID" +msgstr "ID" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__json_options +msgid "Json Options" +msgstr "JSON选项" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_uid +msgid "Last Updated by" +msgstr "最后更新者" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_date +msgid "Last Updated on" +msgstr "最后更新于" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__name +msgid "Name" +msgstr "名称" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options +msgid "Options" +msgstr "选项" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_options_env_default +msgid "Options Env Default" +msgstr "选项环境默认值" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol +msgid "Protocol" +msgstr "协议" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_descr +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Protocol Descr" +msgstr "协议描述" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_protocol_env_default +msgid "Protocol Env Default" +msgstr "协议环境默认值" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path +#: model:ir.model.fields,help:fs_storage.field_fs_storage__x_directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "存储文件的目录的相对路径" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env +msgid "Resolve env vars" +msgstr "解析环境变量" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__eval_options_from_env +msgid "" +"Resolve options values starting with $ from environment variables. e.g\n" +" {\n" +" \"endpoint_url\": \"$AWS_ENDPOINT_URL\",\n" +" }\n" +" " +msgstr "" +"从环境变量解析以$开头的选项值。例如:\n" +"{\n" +" \"endpoint_url\": \"$AWS_ENDPOINT_URL\"\n" +"}\n" +" " + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__server_env_defaults +msgid "Server Env Defaults" +msgstr "服务器环境默认值" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__code +msgid "" +"Technical code used to identify the storage backend into the code.This code " +"must be unique. This code is used for example to define the storage backend " +"to store the attachments via the configuration parameter " +"'ir_attachment.storage.force.database' when the module 'fs_attachment' is " +"installed." +msgstr "" +"用于在代码中标识存储后端的技术代码。此代码必须是唯一的。例如,当安装'fs_attac" +"hment'模块时,此代码用于通过配置参数'ir_attachment.storage.force." +"database'定义存储附件的存储后端。" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Test connection" +msgstr "测试连接" + +#. module: fs_storage +#: model:ir.model.constraint,message:fs_storage.constraint_fs_storage_code_uniq +msgid "The code must be unique" +msgstr "代码必须是唯一的" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "The options must be a valid JSON" +msgstr "选项必须是有效的JSON" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__json_options +msgid "The options used to initialize the filesystem.\n" +msgstr "用于初始化文件系统的选项。\n" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options +#: model:ir.model.fields,help:fs_storage.field_fs_storage__x_options_env_default +msgid "" +"The options used to initialize the filesystem.\n" +"This is a JSON field that depends on the protocol used.\n" +"For example, for the sftp protocol, you can provide the following:\n" +"{\n" +" 'host': 'my.sftp.server',\n" +" 'ssh_kwrags': {\n" +" 'username': 'myuser',\n" +" 'password': 'mypassword',\n" +" 'port': 22,\n" +" }\n" +"}\n" +"For more information, please refer to the fsspec documentation:\n" +"https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations" +msgstr "" +"用于初始化文件系统的选项。\n" +"这是一个依赖于所使用的协议的JSON字段。\n" +"例如,对于sftp协议,您可以提供以下内容:\n" +"{\n" +" 'host': 'my.sftp.server',\n" +" 'ssh_kwrags': {\n" +" 'username': 'myuser',\n" +" 'password': 'mypassword',\n" +" 'port': 22,\n" +" }\n" +"}\n" +"有关更多信息,请参考fsspec文档:\n" +"https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-" +"implementations" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options_protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__x_protocol_env_default +msgid "" +"The protocol used to access the content of filesystem.\n" +"This list is the one supported by the fsspec library (see https://filesystem-spec.readthedocs.io/en/latest). A filesystem protocolis added by default and refers to the odoo local filesystem.\n" +"Pay attention that according to the protocol, some options must beprovided through the options field." +msgstr "" +"用于访问文件系统内容的协议。\n" +"此列表是fsspec库支持的(见 https://filesystem-spec.readthedocs.io/en/" +"latest)。默认添加了一个文件系统协议,指的是Odoo本地文件系统。\n" +"请注意,根据协议,某些选项必须通过选项字段提供。" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_directory_path_env_is_editable +msgid "X Directory Path Env Is Editable" +msgstr "X目录路径环境可编辑" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_options_env_is_editable +msgid "X Options Env Is Editable" +msgstr "X选项环境可编辑" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__x_protocol_env_is_editable +msgid "X Protocol Env Is Editable" +msgstr "X协议环境可编辑" diff --git a/fs_storage/models/__init__.py b/fs_storage/models/__init__.py new file mode 100644 index 0000000000..349bb0495a --- /dev/null +++ b/fs_storage/models/__init__.py @@ -0,0 +1 @@ +from . import fs_storage diff --git a/fs_storage/models/fs_storage.py b/fs_storage/models/fs_storage.py new file mode 100644 index 0000000000..f43f7b4bad --- /dev/null +++ b/fs_storage/models/fs_storage.py @@ -0,0 +1,508 @@ +# Copyright 2023 ACSONE SA/NV (https://www.acsone.eu). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from __future__ import annotations + +import base64 +import functools +import inspect +import json +import logging +import os.path +import re +import warnings +from typing import AnyStr + +import fsspec + +from odoo import _, api, fields, models, tools +from odoo.exceptions import ValidationError + +from odoo.addons.base_sparse_field.models.fields import Serialized + +_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 FSStorage(models.Model): + _name = "fs.storage" + _inherit = "server.env.mixin" + _description = "FS Storage" + + __slots__ = ("__fs", "__odoo_storage_path") + + def __init__(self, env, ids=(), prefetch_ids=()): + super().__init__(env, ids=ids, prefetch_ids=prefetch_ids) + self.__fs = None + self.__odoo_storage_path = None + + name = fields.Char(required=True) + code = fields.Char( + required=True, + help="Technical code used to identify the storage backend into the code." + "This code must be unique. This code is used for example to define the " + "storage backend to store the attachments via the configuration parameter " + "'ir_attachment.storage.force.database' when the module 'fs_attachment' " + "is installed.", + ) + protocol = fields.Selection( + selection="_get_protocols", + required=True, + help="The protocol used to access the content of filesystem.\n" + "This list is the one supported by the fsspec library (see " + "https://filesystem-spec.readthedocs.io/en/latest). A filesystem protocol" + "is added by default and refers to the odoo local filesystem.\n" + "Pay attention that according to the protocol, some options must be" + "provided through the options field.", + ) + protocol_descr = fields.Text( + compute="_compute_protocol_descr", + ) + options = fields.Text( + help="The options used to initialize the filesystem.\n" + "This is a JSON field that depends on the protocol used.\n" + "For example, for the sftp protocol, you can provide the following:\n" + "{\n" + " 'host': 'my.sftp.server',\n" + " 'ssh_kwrags': {\n" + " 'username': 'myuser',\n" + " 'password': 'mypassword',\n" + " 'port': 22,\n" + " }\n" + "}\n" + "For more information, please refer to the fsspec documentation:\n" + "https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations" + ) + + json_options = Serialized( + help="The options used to initialize the filesystem.\n", + compute="_compute_json_options", + inverse="_inverse_json_options", + ) + + eval_options_from_env = fields.Boolean( + string="Resolve env vars", + help="""Resolve options values starting with $ from environment variables. e.g + { + "endpoint_url": "$AWS_ENDPOINT_URL", + } + """, + ) + + directory_path = fields.Char( + help="Relative path to the directory to store the file" + ) + + # the next fields are used to display documentation to help the user + # to configure the backend + options_protocol = fields.Selection( + string="Describes Protocol", + selection="_get_options_protocol", + compute="_compute_protocol_descr", + help="The protocol used to access the content of filesystem.\n" + "This list is the one supported by the fsspec library (see " + "https://filesystem-spec.readthedocs.io/en/latest). A filesystem protocol" + "is added by default and refers to the odoo local filesystem.\n" + "Pay attention that according to the protocol, some options must be" + "provided through the options field.", + ) + options_properties = fields.Text( + string="Available properties", + compute="_compute_options_properties", + store=False, + ) + check_connection_method = fields.Selection( + selection="_get_check_connection_method_selection", + default="marker_file", + help="Set a method if you want the connection to remote to be checked every " + "time the storage is used, in order to remove the obsolete connection from" + " the cache.\n" + "* Create Marker file : Create a file on remote and check it exists\n" + "* List File : List all files from root directory", + ) + + _sql_constraints = [ + ( + "code_uniq", + "unique(code)", + "The code must be unique", + ), + ] + + _server_env_section_name_field = "code" + + @api.model + def _get_check_connection_method_selection(self): + return [ + ("marker_file", _("Create Marker file")), + ("ls", _("List File")), + ] + + @property + def _server_env_fields(self): + return {"protocol": {}, "options": {}, "directory_path": {}} + + def write(self, vals): + self.__fs = None + self.env.registry.clear_cache() + return super().write(vals) + + @api.model + @tools.ormcache() + def get_id_by_code_map(self): + """Return a dictionary with the code as key and the id as value.""" + return {rec.code: rec.id for rec in self.sudo().search([])} + + @api.model + def get_id_by_code(self, code): + """Return the id of the filesystem associated to the given code.""" + return self.get_id_by_code_map().get(code) + + @api.model + def get_by_code(self, code) -> FSStorage: + """Return the filesystem associated to the given code.""" + res = self.browse() + res_id = self.get_id_by_code(code) + if res_id: + res = self.browse(res_id) + return res + + @api.model + @tools.ormcache() + def get_storage_codes(self): + """Return the list of codes of the existing filesystems.""" + return [s.code for s in self.search([])] + + @api.model + @tools.ormcache("code") + def get_fs_by_code(self, code): + """Return the filesystem associated to the given code. + + :param code: the code of the filesystem + """ + fs = None + fs_storage = self.get_by_code(code) + if fs_storage: + fs = fs_storage.fs + return fs + + def copy(self, default=None): + default = default or {} + if "code" not in default: + default["code"] = f"{self.code}_copy" + return super().copy(default) + + @api.model + def _get_protocols(self) -> list[tuple[str, str]]: + protocol = [("odoofs", "Odoo's FileSystem")] + for p in fsspec.available_protocols(): + try: + cls = fsspec.get_filesystem_class(p) + protocol.append((p, f"{p} ({cls.__name__})")) + except ImportError as e: + _logger.debug("Cannot load the protocol %s. Reason: %s", p, e) + return protocol + + @api.constrains("options") + def _check_options(self) -> None: + for rec in self: + try: + json.loads(rec.options or "{}") + except Exception as e: + raise ValidationError(_("The options must be a valid JSON")) from e + + @api.depends("options") + def _compute_json_options(self) -> None: + for rec in self: + rec.json_options = json.loads(rec.options or "{}") + + def _inverse_json_options(self) -> None: + for rec in self: + rec.options = json.dumps(rec.json_options) + + @api.depends("protocol") + def _compute_protocol_descr(self) -> None: + for rec in self: + rec.protocol_descr = fsspec.get_filesystem_class(rec.protocol).__doc__ + rec.options_protocol = rec.protocol + + @api.model + def _get_options_protocol(self) -> list[tuple[str, str]]: + protocol = [("odoofs", "Odoo's Filesystem")] + for p in fsspec.available_protocols(): + try: + fsspec.get_filesystem_class(p) + protocol.append((p, p)) + except ImportError as e: + _logger.debug("Cannot load the protocol %s. Reason: %s", p, e) + return protocol + + @api.depends("options_protocol") + def _compute_options_properties(self) -> None: + for rec in self: + cls = fsspec.get_filesystem_class(rec.options_protocol) + signature = inspect.signature(cls.__init__) + doc = inspect.getdoc(cls.__init__) + rec.options_properties = f"__init__{signature}\n{doc}" + + def _get_marker_file_name(self): + return f".odoo_fs_storage_{self.id}.marker" + + def _marker_file_check_connection(self, fs): + marker_file_name = self._get_marker_file_name() + try: + fs.info(marker_file_name) + except FileNotFoundError: + fs.touch(marker_file_name) + + def _ls_check_connection(self, fs): + fs.ls("", detail=False) + + def _check_connection(self, fs, check_connection_method): + if check_connection_method == "marker_file": + self._marker_file_check_connection(fs) + elif check_connection_method == "ls": + self._ls_check_connection(fs) + return True + + @property + def fs(self) -> fsspec.AbstractFileSystem: + """Get the fsspec filesystem for this backend.""" + self.ensure_one() + if not self.__fs: + self.__fs = self.sudo()._get_filesystem() + if not tools.config["test_enable"]: + # Check whether we need to invalidate FS cache or not. + # Use a marker file to limit the scope of the LS command for performance. + try: + self._check_connection(self.__fs, self.check_connection_method) + except Exception as e: + self.__fs.clear_instance_cache() + self.__fs = None + raise e + return self.__fs + + def _get_filesystem_storage_path(self) -> str: + """Get the path to the storage directory. + + This path is relative to the odoo filestore.and is used as root path + when the protocol is filesystem. + """ + self.ensure_one() + path = os.path.join(self.env["ir.attachment"]._filestore(), "storage") + if not os.path.exists(path): + os.makedirs(path) + return path + + @property + def _odoo_storage_path(self) -> str: + """Get the path to the storage directory. + + This path is relative to the odoo filestore.and is used as root path + when the protocol is filesystem. + """ + if not self.__odoo_storage_path: + self.__odoo_storage_path = self._get_filesystem_storage_path() + return self.__odoo_storage_path + + def _recursive_add_odoo_storage_path(self, options: dict) -> dict: + """Add the odoo storage path to the options. + + This is a recursive function that will add the odoo_storage_path + option to the nested target_options if the target_protocol is + odoofs + """ + if "target_protocol" in options: + target_options = options.get("target_options", {}) + if options["target_protocol"] == "odoofs": + target_options["odoo_storage_path"] = self._odoo_storage_path + options["target_options"] = target_options + self._recursive_add_odoo_storage_path(target_options) + return options + + def _eval_options_from_env(self, options): + values = {} + for key, value in options.items(): + if isinstance(value, dict): + values[key] = self._eval_options_from_env(value) + elif isinstance(value, str) and value.startswith("$"): + env_variable_name = value[1:] + env_variable_value = os.getenv(env_variable_name) + if env_variable_value is not None: + values[key] = env_variable_value + else: + values[key] = value + _logger.warning( + "Environment variable %s is not set for fs_storage %s.", + env_variable_name, + self.display_name, + ) + else: + values[key] = value + return values + + def _get_fs_options(self): + options = self.json_options + if not self.eval_options_from_env: + return options + return self._eval_options_from_env(self.json_options) + + def _get_filesystem(self) -> fsspec.AbstractFileSystem: + """Get the fsspec filesystem for this backend. + + See https://filesystem-spec.readthedocs.io/en/latest/api.html + #fsspec.spec.AbstractFileSystem + + :return: fsspec.AbstractFileSystem + """ + self.ensure_one() + options = self._get_fs_options() + if self.protocol == "odoofs": + options["odoo_storage_path"] = self._odoo_storage_path + # Webdav protocol handler does need the auth to be a tuple not a list ! + if ( + self.protocol == "webdav" + and "auth" in options + and isinstance(options["auth"], list) + ): + options["auth"] = tuple(options["auth"]) + options = self._recursive_add_odoo_storage_path(options) + fs = fsspec.filesystem(self.protocol, **options) + directory_path = self.directory_path + if directory_path: + fs = fsspec.filesystem("rooted_dir", path=directory_path, fs=fs) + return fs + + # Deprecated methods used to ease the migration from the storage_backend addons + # to the fs_storage addons. These methods will be removed in the future (Odoo 18) + @deprecated("Please use _get_filesystem() instead and the fsspec API directly.") + def add(self, relative_path, data, binary=True, **kwargs) -> None: + if not binary: + data = base64.b64decode(data) + path = relative_path.split(self.fs.sep)[:-1] + if not self.fs.exists(self.fs.sep.join(path)): + self.fs.makedirs(self.fs.sep.join(path)) + with self.fs.open(relative_path, "wb", **kwargs) as f: + f.write(data) + + @deprecated("Please use _get_filesystem() instead and the fsspec API directly.") + def get(self, relative_path, binary=True, **kwargs) -> AnyStr: + data = self.fs.read_bytes(relative_path, **kwargs) + if not binary and data: + data = base64.b64encode(data) + return data + + @deprecated("Please use _get_filesystem() instead and the fsspec API directly.") + def list_files(self, relative_path="", pattern=False) -> list[str]: + relative_path = relative_path or self.fs.root_marker + if not self.fs.exists(relative_path): + return [] + if pattern: + relative_path = self.fs.sep.join([relative_path, pattern]) + return self.fs.glob(relative_path) + return self.fs.ls(relative_path, detail=False) + + @deprecated("Please use _get_filesystem() instead and the fsspec API directly.") + def find_files(self, pattern, relative_path="", **kw) -> list[str]: + """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 + """ + result = [] + relative_path = relative_path or self.fs.root_marker + if not self.fs.exists(relative_path): + return [] + regex = re.compile(pattern) + for file_path in self.fs.ls(relative_path, detail=False): + # fs.ls returns a relative path + if regex.match(os.path.basename(file_path)): + result.append(file_path) + return result + + @deprecated("Please use _get_filesystem() instead and the fsspec API directly.") + def move_files(self, files, destination_path, **kw) -> None: + """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 + """ + for file_path in files: + self.fs.move( + file_path, + self.fs.sep.join([destination_path, os.path.basename(file_path)]), + **kw, + ) + + @deprecated("Please use _get_filesystem() instead and the fsspec API directly.") + def delete(self, relative_path) -> None: + self.fs.rm_file(relative_path) + + def action_test_config(self): + self.ensure_one() + if self.check_connection_method: + return self._test_config(self.check_connection_method) + else: + action = self.env["ir.actions.actions"]._for_xml_id( + "fs_storage.act_open_fs_test_connection_view" + ) + action["context"] = {"active_model": "fs.storage", "active_id": self.id} + return action + + def _test_config(self, connection_method): + try: + self._check_connection(self.fs, connection_method) + title = _("Connection Test Succeeded!") + message = _("Everything seems properly set up!") + msg_type = "success" + except Exception as err: + title = _("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/fs_storage/odoo_file_system.py b/fs_storage/odoo_file_system.py new file mode 100644 index 0000000000..c6738c2243 --- /dev/null +++ b/fs_storage/odoo_file_system.py @@ -0,0 +1,50 @@ +# Copyright 2023 ACSONE SA/NV (https://www.acsone.eu). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +from fsspec.registry import register_implementation + +from .rooted_dir_file_system import RootedDirFileSystem + + +class OdooFileSystem(RootedDirFileSystem): + """A directory-based filesystem for Odoo. + + This filesystem is mounted from a specific subdirectory of the Odoo + filestore directory. + + It extends the RootedDirFileSystem to avoid going outside the + specific subdirectory nor the Odoo filestore directory. + + Parameters: + odoo_storage_path: The path of the subdirectory of the Odoo filestore + directory to mount. This parameter is required and is always provided + by the Odoo FS Storage even if it is explicitly defined in the + storage options. + fs: AbstractFileSystem + An instantiated filesystem to wrap. + target_protocol, target_options: + if fs is none, construct it from these + """ + + def __init__( + self, + *, + odoo_storage_path, + fs=None, + target_protocol=None, + target_options=None, + **storage_options, + ): + if not odoo_storage_path: + raise ValueError("odoo_storage_path is required") + super().__init__( + path=odoo_storage_path, + fs=fs, + target_protocol=target_protocol, + target_options=target_options, + **storage_options, + ) + + +register_implementation("odoofs", OdooFileSystem) diff --git a/fs_storage/pyproject.toml b/fs_storage/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/fs_storage/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fs_storage/readme/CONTRIBUTORS.md b/fs_storage/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..a65c00663e --- /dev/null +++ b/fs_storage/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Laurent Mignon \<\> +- Sébastien BEAU \<\> diff --git a/fs_storage/readme/DESCRIPTION.md b/fs_storage/readme/DESCRIPTION.md new file mode 100644 index 0000000000..b2967b0e4e --- /dev/null +++ b/fs_storage/readme/DESCRIPTION.md @@ -0,0 +1,65 @@ +This addon is a technical addon that allows you to define filesystem +like storage for your data. It's used by other addons to store their +data in a transparent way into different kind of storages. + +Through the fs.storage record, you get access to an object that +implements +the [fsspec.spec.AbstractFileSystem](https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.spec.AbstractFileSystem) +interface and +therefore give you an unified interface to access your data whatever the +storage protocol you decide to use. + +The list of supported protocols depends on the installed fsspec +implementations. By default, the addon will install the following +protocols: + +- LocalFileSystem +- MemoryFileSystem +- ZipFileSystem +- TarFileSystem +- FTPFileSystem +- CachingFileSystem +- WholeFileSystem +- SimplCacheFileSystem +- ReferenceFileSystem +- GenericFileSystem +- DirFileSystem +- DatabricksFileSystem +- GitHubFileSystem +- JupiterFileSystem +- OdooFileSystem + +The OdooFileSystem is the one that allows you to store your data into a +directory mounted into your Odoo's storage directory. This is the +default FS Storage when creating a new fs.storage record. + +Others protocols are available through the installation of additional +python packages: + +- DropboxDriveFileSystem -\> pip install fsspec\[dropbox\] +- HTTPFileSystem -\> pip install fsspec\[http\] +- HTTPSFileSystem -\> pip install fsspec\[http\] +- GCSFileSystem -\> pip install fsspec\[gcs\] +- GSFileSystem -\> pip install fsspec\[gs\] +- GoogleDriveFileSystem -\> pip install gdrivefs +- SFTPFileSystem -\> pip install fsspec\[sftp\] +- HaddoopFileSystem -\> pip install fsspec\[hdfs\] +- S3FileSystem -\> pip install fsspec\[s3\] +- WandbFS -\> pip install wandbfs +- OCIFileSystem -\> pip install fsspec\[oci\] +- AsyncLocalFileSystem -\> pip install 'morefs\[asynclocalfs\] +- AzureDatalakeFileSystem -\> pip install fsspec\[adl\] +- AzureBlobFileSystem -\> pip install fsspec\[abfs\] +- DaskWorkerFileSystem -\> pip install fsspec\[dask\] +- GitFileSystem -\> pip install fsspec\[git\] +- SMBFileSystem -\> pip install fsspec\[smb\] +- LibArchiveFileSystem -\> pip install fsspec\[libarchive\] +- OSSFileSystem -\> pip install ossfs +- WebdavFileSystem -\> pip install webdav4 +- DVCFileSystem -\> pip install dvc +- XRootDFileSystem -\> pip install fsspec-xrootd + +This list of supported protocols is not exhaustive or could change in +the future depending on the fsspec releases. You can find more +information about the supported protocols on the [fsspec +documentation](https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.spec.AbstractFileSystem). diff --git a/fs_storage/readme/HISTORY.md b/fs_storage/readme/HISTORY.md new file mode 100644 index 0000000000..432d8e2ead --- /dev/null +++ b/fs_storage/readme/HISTORY.md @@ -0,0 +1,25 @@ +## 16.0.1.1.0 (2023-12-22) + +**Features** + +- Add parameter on storage backend to resolve protocol options values + starting with \$ from environment variables + ([\#303](https://github.com/OCA/storage/issues/303)) + +## 16.0.1.0.3 (2023-10-17) + +**Bugfixes** + +- Fix access to technical models to be able to upload attachments for + users with basic access + ([\#289](https://github.com/OCA/storage/issues/289)) + +## 16.0.1.0.2 (2023-10-09) + +**Bugfixes** + +- Avoid config error when using the webdav protocol. The auth option is + expected to be a tuple not a list. Since our config is loaded from a + json file, we cannot use tuples. The fix converts the list to a tuple + when the config is related to a webdav protocol and the auth option is + into the confix. ([\#285](https://github.com/OCA/storage/issues/285)) diff --git a/fs_storage/readme/ROADMAP.md b/fs_storage/readme/ROADMAP.md new file mode 100644 index 0000000000..8f807c23e8 --- /dev/null +++ b/fs_storage/readme/ROADMAP.md @@ -0,0 +1,10 @@ +- Transactions: fsspec comes with a transactional mechanism that once + started, gathers all the files created during the transaction, and if + the transaction is committed, moves them to their final locations. It + would be useful to bridge this with the transactional mechanism of + odoo. This would allow to ensure that all the files created during a + transaction are either all moved to their final locations, or all + deleted if the transaction is rolled back. This mechanism is only + valid for files created during the transaction by a call to the open + method of the file system. It is not valid for others operations, such + as rm, mv_file, ... . diff --git a/fs_storage/readme/USAGE.md b/fs_storage/readme/USAGE.md new file mode 100644 index 0000000000..ac73b9bc4b --- /dev/null +++ b/fs_storage/readme/USAGE.md @@ -0,0 +1,100 @@ +## Configuration + +When you create a new backend, you must specify the following: + +- The name of the backend. This is the name that will be used to + identify the backend into Odoo +- The code of the backend. This code will identify the backend into the + store_fname field of the ir.attachment model. This code must be + unique. It will be used as scheme. example of the store_fname field: + `odoofs://abs34Tg11`. +- The protocol used by the backend. The protocol refers to the supported + protocols of the fsspec python package. +- A directory path. This is a root directory from which the filesystem + will be mounted. This directory must exist. +- The protocol options. These are the options that will be passed to the + fsspec python package when creating the filesystem. These options + depend on the protocol used and are described in the fsspec + documentation. +- Resolve env vars. This options resolves the protocol options values + starting with \$ from environment variables +- Check Connection Method. If set, Odoo will always check the connection before + using a storage and it will remove the fs connection from the cache if the + check fails. + + - `Create Marker file`: create a hidden file on remote and then check it + exists with Use it if you have write access to the remote and if it is not + an issue to leave the marker file in the root directory. + - `List file`: list all files from the root directory. You can use it if the + directory path does not contain a big list of files (for performance + reasons) + +Some protocols defined in the fsspec package are wrappers around other +protocols. For example, the SimpleCacheFileSystem protocol is a wrapper +around any local filesystem protocol. In such cases, you must specify +into the protocol options the protocol to be wrapped and the options to +be passed to the wrapped protocol. + +For example, if you want to create a backend that uses the +SimpleCacheFileSystem protocol, after selecting the +SimpleCacheFileSystem protocol, you must specify the protocol options as +follows: + +``` python +{ + "directory_path": "/tmp/my_backend", + "target_protocol": "odoofs", + "target_options": {...}, +} +``` + +In this example, the SimpleCacheFileSystem protocol will be used as a +wrapper around the odoofs protocol. + +## Server Environment + +To ease the management of the filesystem storages configuration accross +the different environments, the configuration of the filesystem storages +can be defined in environment files or directly in the main +configuration file. For example, the configuration of a filesystem +storage with the code fsprod can be provided in the main configuration +file as follows: + +``` ini +[fs_storage.fsprod] +protocol=s3 +options={"endpoint_url": "https://my_s3_server/", "key": "KEY", "secret": "SECRET"} +directory_path=my_bucket +``` + +To work, a storage.backend record must exist with the code fsprod into +the database. In your configuration section, you can specify the value +for the following fields: + +- protocol +- options +- directory_path + +## Migration from storage_backend + +The fs_storage addon can be used to replace the storage_backend addon. +(It has been designed to be a drop-in replacement for the +storage_backend addon). To ease the migration, the fs.storage model +defines the high-level methods available in the storage_backend model. +These methods are: + +- add +- get +- list_files +- find_files +- move_files +- delete + +These methods are wrappers around the methods of the +fsspec.AbstractFileSystem class (see +). +These methods are marked as deprecated and will be removed in a future +version (V18) of the addon. You should use the methods of the +fsspec.AbstractFileSystem class instead since they are more flexible and +powerful. You can access the instance of the fsspec.AbstractFileSystem +class using the fs property of a fs.storage record. diff --git a/fs_storage/readme/newsfragments/.gitignore b/fs_storage/readme/newsfragments/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/fs_storage/readme/newsfragments/320.feature b/fs_storage/readme/newsfragments/320.feature new file mode 100644 index 0000000000..5644d66085 --- /dev/null +++ b/fs_storage/readme/newsfragments/320.feature @@ -0,0 +1 @@ +Invalidate FS filesystem object cache when the connection fails, forcing a reconnection. diff --git a/fs_storage/rooted_dir_file_system.py b/fs_storage/rooted_dir_file_system.py new file mode 100644 index 0000000000..91bae52200 --- /dev/null +++ b/fs_storage/rooted_dir_file_system.py @@ -0,0 +1,37 @@ +# Copyright 2023 ACSONE SA/NV (https://www.acsone.eu). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import os + +from fsspec.implementations.dirfs import DirFileSystem +from fsspec.implementations.local import make_path_posix +from fsspec.registry import register_implementation + + +class RootedDirFileSystem(DirFileSystem): + """A directory-based filesystem that uses path as a root. + + The main purpose of this filesystem is to ensure that paths are always + a sub path of the initial path. IOW, it is not possible to go outside + the initial path. That's the only difference with the DirFileSystem provided + by fsspec. + + This one should be provided by fsspec itself. We should propose a PR. + """ + + def _join(self, path): + path = super()._join(path) + # Ensure that the path is a subpath of the root path by resolving + # any relative paths. + # Since the path separator is not always the same on all systems, + # we need to normalize the path separator. + path_posix = os.path.normpath(make_path_posix(path)) + root_posix = os.path.normpath(make_path_posix(self.path)) + if not path_posix.startswith(root_posix): + raise PermissionError( + f"Path {path} is not a subpath of the root path {self.path}" + ) + return path + + +register_implementation("rooted_dir", RootedDirFileSystem) diff --git a/fs_storage/security/ir.model.access.csv b/fs_storage/security/ir.model.access.csv new file mode 100644 index 0000000000..c1a81aae11 --- /dev/null +++ b/fs_storage/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fs_storage_edit,fs_storage edit,model_fs_storage,base.group_system,1,1,1,1 +access_fs_test_connection,fs.test.connection.access,model_fs_test_connection,base.group_system,1,1,1,1 diff --git a/fs_storage/static/description/icon.png b/fs_storage/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/fs_storage/static/description/icon.png differ diff --git a/fs_storage/static/description/index.html b/fs_storage/static/description/index.html new file mode 100644 index 0000000000..bd85d3c61c --- /dev/null +++ b/fs_storage/static/description/index.html @@ -0,0 +1,647 @@ + + + + + +Filesystem Storage Backend + + + +
+

Filesystem Storage Backend

+ + +

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

+

This addon is a technical addon that allows you to define filesystem +like storage for your data. It’s used by other addons to store their +data in a transparent way into different kind of storages.

+

Through the fs.storage record, you get access to an object that +implements the +fsspec.spec.AbstractFileSystem +interface and therefore give you an unified interface to access your +data whatever the storage protocol you decide to use.

+

The list of supported protocols depends on the installed fsspec +implementations. By default, the addon will install the following +protocols:

+
    +
  • LocalFileSystem
  • +
  • MemoryFileSystem
  • +
  • ZipFileSystem
  • +
  • TarFileSystem
  • +
  • FTPFileSystem
  • +
  • CachingFileSystem
  • +
  • WholeFileSystem
  • +
  • SimplCacheFileSystem
  • +
  • ReferenceFileSystem
  • +
  • GenericFileSystem
  • +
  • DirFileSystem
  • +
  • DatabricksFileSystem
  • +
  • GitHubFileSystem
  • +
  • JupiterFileSystem
  • +
  • OdooFileSystem
  • +
+

The OdooFileSystem is the one that allows you to store your data into a +directory mounted into your Odoo’s storage directory. This is the +default FS Storage when creating a new fs.storage record.

+

Others protocols are available through the installation of additional +python packages:

+
    +
  • DropboxDriveFileSystem -> pip install fsspec[dropbox]
  • +
  • HTTPFileSystem -> pip install fsspec[http]
  • +
  • HTTPSFileSystem -> pip install fsspec[http]
  • +
  • GCSFileSystem -> pip install fsspec[gcs]
  • +
  • GSFileSystem -> pip install fsspec[gs]
  • +
  • GoogleDriveFileSystem -> pip install gdrivefs
  • +
  • SFTPFileSystem -> pip install fsspec[sftp]
  • +
  • HaddoopFileSystem -> pip install fsspec[hdfs]
  • +
  • S3FileSystem -> pip install fsspec[s3]
  • +
  • WandbFS -> pip install wandbfs
  • +
  • OCIFileSystem -> pip install fsspec[oci]
  • +
  • AsyncLocalFileSystem -> pip install ‘morefs[asynclocalfs]
  • +
  • AzureDatalakeFileSystem -> pip install fsspec[adl]
  • +
  • AzureBlobFileSystem -> pip install fsspec[abfs]
  • +
  • DaskWorkerFileSystem -> pip install fsspec[dask]
  • +
  • GitFileSystem -> pip install fsspec[git]
  • +
  • SMBFileSystem -> pip install fsspec[smb]
  • +
  • LibArchiveFileSystem -> pip install fsspec[libarchive]
  • +
  • OSSFileSystem -> pip install ossfs
  • +
  • WebdavFileSystem -> pip install webdav4
  • +
  • DVCFileSystem -> pip install dvc
  • +
  • XRootDFileSystem -> pip install fsspec-xrootd
  • +
+

This list of supported protocols is not exhaustive or could change in +the future depending on the fsspec releases. You can find more +information about the supported protocols on the fsspec +documentation.

+

Table of contents

+ +
+

Usage

+
+

Configuration

+

When you create a new backend, you must specify the following:

+
    +
  • The name of the backend. This is the name that will be used to +identify the backend into Odoo
  • +
  • The code of the backend. This code will identify the backend into the +store_fname field of the ir.attachment model. This code must be +unique. It will be used as scheme. example of the store_fname field: +odoofs://abs34Tg11.
  • +
  • The protocol used by the backend. The protocol refers to the +supported protocols of the fsspec python package.
  • +
  • A directory path. This is a root directory from which the filesystem +will be mounted. This directory must exist.
  • +
  • The protocol options. These are the options that will be passed to +the fsspec python package when creating the filesystem. These options +depend on the protocol used and are described in the fsspec +documentation.
  • +
  • Resolve env vars. This options resolves the protocol options values +starting with $ from environment variables
  • +
  • Check Connection Method. If set, Odoo will always check the +connection before using a storage and it will remove the fs +connection from the cache if the check fails.
      +
    • Create Marker file: create a hidden file on remote and then +check it exists with Use it if you have write access to the remote +and if it is not an issue to leave the marker file in the root +directory.
    • +
    • List file: list all files from the root directory. You can use +it if the directory path does not contain a big list of files (for +performance reasons)
    • +
    +
  • +
+

Some protocols defined in the fsspec package are wrappers around other +protocols. For example, the SimpleCacheFileSystem protocol is a wrapper +around any local filesystem protocol. In such cases, you must specify +into the protocol options the protocol to be wrapped and the options to +be passed to the wrapped protocol.

+

For example, if you want to create a backend that uses the +SimpleCacheFileSystem protocol, after selecting the +SimpleCacheFileSystem protocol, you must specify the protocol options as +follows:

+
+{
+    "directory_path": "/tmp/my_backend",
+    "target_protocol": "odoofs",
+    "target_options": {...},
+}
+
+

In this example, the SimpleCacheFileSystem protocol will be used as a +wrapper around the odoofs protocol.

+
+
+

Server Environment

+

To ease the management of the filesystem storages configuration accross +the different environments, the configuration of the filesystem storages +can be defined in environment files or directly in the main +configuration file. For example, the configuration of a filesystem +storage with the code fsprod can be provided in the main configuration +file as follows:

+
+[fs_storage.fsprod]
+protocol=s3
+options={"endpoint_url": "https://my_s3_server/", "key": "KEY", "secret": "SECRET"}
+directory_path=my_bucket
+
+

To work, a storage.backend record must exist with the code fsprod into +the database. In your configuration section, you can specify the value +for the following fields:

+
    +
  • protocol
  • +
  • options
  • +
  • directory_path
  • +
+
+
+

Migration from storage_backend

+

The fs_storage addon can be used to replace the storage_backend addon. +(It has been designed to be a drop-in replacement for the +storage_backend addon). To ease the migration, the fs.storage model +defines the high-level methods available in the storage_backend model. +These methods are:

+
    +
  • add
  • +
  • get
  • +
  • list_files
  • +
  • find_files
  • +
  • move_files
  • +
  • delete
  • +
+

These methods are wrappers around the methods of the +fsspec.AbstractFileSystem class (see +https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.spec.AbstractFileSystem). +These methods are marked as deprecated and will be removed in a future +version (V18) of the addon. You should use the methods of the +fsspec.AbstractFileSystem class instead since they are more flexible and +powerful. You can access the instance of the fsspec.AbstractFileSystem +class using the fs property of a fs.storage record.

+
+
+
+

Known issues / Roadmap

+
    +
  • Transactions: fsspec comes with a transactional mechanism that once +started, gathers all the files created during the transaction, and if +the transaction is committed, moves them to their final locations. It +would be useful to bridge this with the transactional mechanism of +odoo. This would allow to ensure that all the files created during a +transaction are either all moved to their final locations, or all +deleted if the transaction is rolled back. This mechanism is only +valid for files created during the transaction by a call to the open +method of the file system. It is not valid for others operations, +such as rm, mv_file, … .
  • +
+
+
+

Changelog

+
+

16.0.1.1.0 (2023-12-22)

+

Features

+
    +
  • Add parameter on storage backend to resolve protocol options values +starting with $ from environment variables +(#303)
  • +
+
+
+

16.0.1.0.3 (2023-10-17)

+

Bugfixes

+
    +
  • Fix access to technical models to be able to upload attachments for +users with basic access +(#289)
  • +
+
+
+

16.0.1.0.2 (2023-10-09)

+

Bugfixes

+
    +
  • Avoid config error when using the webdav protocol. The auth option is +expected to be a tuple not a list. Since our config is loaded from a +json file, we cannot use tuples. The fix converts the list to a tuple +when the config is related to a webdav protocol and the auth option +is into the confix. +(#285)
  • +
+
+
+
+

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

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

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/tests/__init__.py b/fs_storage/tests/__init__.py new file mode 100644 index 0000000000..ffb9e7e52e --- /dev/null +++ b/fs_storage/tests/__init__.py @@ -0,0 +1,2 @@ +from . import common +from . import test_fs_storage diff --git a/fs_storage/tests/common.py b/fs_storage/tests/common.py new file mode 100644 index 0000000000..03dd68762f --- /dev/null +++ b/fs_storage/tests/common.py @@ -0,0 +1,46 @@ +# Copyright 2023 ACSONE SA/NV (http://acsone.eu). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +import base64 +import os +import shutil +import tempfile +from unittest import mock + +from odoo.tests.common import TransactionCase + +from ..models.fs_storage import FSStorage + + +class TestFSStorageCase(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.backend: FSStorage = cls.env.ref("fs_storage.fs_storage_demo") + cls.backend.json_options = {"target_options": {"auto_mkdir": "True"}} + 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") + cls.temp_dir = tempfile.mkdtemp() + + def setUp(self): + super().setUp() + mocked_backend = mock.patch.object( + self.backend.__class__, "_get_filesystem_storage_path" + ) + mocked_get_filesystem_storage_path = mocked_backend.start() + mocked_get_filesystem_storage_path.return_value = self.temp_dir + self.backend.write({"directory_path": self.temp_dir}) + + # pylint: disable=unused-variable + @self.addCleanup + def stop_mock(): + mocked_backend.stop() + # recursively delete the tempdir + if os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + + def _create_file(self, backend: FSStorage, filename: str, filedata: str): + with backend.fs.open(filename, "wb") as f: + f.write(filedata) diff --git a/fs_storage/tests/test_fs_storage.py b/fs_storage/tests/test_fs_storage.py new file mode 100644 index 0000000000..0623080ee5 --- /dev/null +++ b/fs_storage/tests/test_fs_storage.py @@ -0,0 +1,152 @@ +# Copyright 2023 ACSONE SA/NV (http://acsone.eu). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +import warnings +from unittest import mock + +from odoo.tests import Form +from odoo.tools import mute_logger + +from .common import TestFSStorageCase + + +class TestFSStorage(TestFSStorageCase): + @mute_logger("py.warnings") + def _test_deprecated_setting_and_getting_data(self): + # Check that the directory is empty + warnings.filterwarnings("ignore") + 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) + + @mute_logger("py.warnings") + def _test_deprecated_find_files(self): + warnings.filterwarnings("ignore") + self.backend.add( + self.filename, self.filedata, mimetype="text/plain", binary=False + ) + try: + res = self.backend.find_files(r".*\.txt") + self.assertListEqual([self.filename], res) + res = self.backend.find_files(r".*\.text") + self.assertListEqual([], res) + finally: + self.backend.delete(self.filename) + + def test_deprecated_setting_and_getting_data_from_root(self): + self._test_deprecated_setting_and_getting_data() + + def test_deprecated_setting_and_getting_data_from_dir(self): + self.backend.directory_path = self.case_with_subdirectory + self._test_deprecated_setting_and_getting_data() + + def test_deprecated_find_files_from_root(self): + self._test_deprecated_find_files() + + def test_deprecated_find_files_from_dir(self): + self.backend.directory_path = self.case_with_subdirectory + self._test_deprecated_find_files() + + def test_ensure_one_fs_by_record(self): + # in this test we ensure that we've one fs by record + backend_ids = [] + for i in range(4): + backend_ids.append( + self.backend.create( + {"name": f"name{i}", "directory_path": f"{i}", "code": f"code{i}"} + ).id + ) + records = self.backend.browse(backend_ids) + fs = None + for rec in records: + self.assertNotEqual(fs, rec.fs) + + def test_relative_access(self): + self.backend.directory_path = self.case_with_subdirectory + self._create_file(self.backend, self.filename, self.filedata) + other_subdirectory = "other_subdirectory" + backend2 = self.backend.copy({"directory_path": other_subdirectory}) + self._create_file(backend2, self.filename, self.filedata) + with self.assertRaises(PermissionError), self.env.cr.savepoint(): + # check that we can't access outside the subdirectory + backend2.fs.ls("../") + with self.assertRaises(PermissionError), self.env.cr.savepoint(): + # check that we can't access the file into another subdirectory + backend2.fs.ls(f"../{self.case_with_subdirectory}") + self.backend.fs.rm_file(self.filename) + backend2.fs.rm_file(self.filename) + + def test_recursive_add_odoo_storage_path_to_options(self): + options = { + "directory_path": "/tmp/my_backend", + "target_protocol": "odoofs", + } + self.backend._recursive_add_odoo_storage_path(options) + self.assertEqual( + self.backend._odoo_storage_path, + options.get("target_options").get("odoo_storage_path"), + ) + options = { + "directory_path": "/tmp/my_backend", + "target_protocol": "dir", + "target_options": { + "path": "/my_backend", + "target_protocol": "odoofs", + }, + } + self.backend._recursive_add_odoo_storage_path(options) + self.assertEqual( + self.backend._odoo_storage_path, + options.get("target_options") + .get("target_options") + .get("odoo_storage_path"), + ) + + def test_interface_values(self): + protocol = "file" # should be available by default in the list of protocols + with Form(self.env["fs.storage"]) as new_storage: + new_storage.name = "Test storage" + new_storage.code = "code" + new_storage.protocol = protocol + self.assertEqual(new_storage.protocol, protocol) + # the options should follow the protocol + self.assertEqual(new_storage.options_protocol, protocol) + description = new_storage.protocol_descr + self.assertTrue("Interface to files on local storage" in description) + # this is still true after saving + self.assertEqual(new_storage.options_protocol, protocol) + + def test_options_env(self): + self.backend.json_options = {"key": {"sub_key": "$KEY_VAR"}} + eval_json_options = {"key": {"sub_key": "TEST"}} + options = self.backend._get_fs_options() + self.assertDictEqual(options, self.backend.json_options) + self.backend.eval_options_from_env = True + with mock.patch.dict("os.environ", {"KEY_VAR": "TEST"}): + options = self.backend._get_fs_options() + self.assertDictEqual(options, eval_json_options) + with self.assertLogs(level="WARNING") as log: + options = self.backend._get_fs_options() + self.assertIn( + ( + f"Environment variable KEY_VAR is not set for " + f"fs_storage {self.backend.display_name}." + ), + log.output[0], + ) diff --git a/fs_storage/views/fs_storage_view.xml b/fs_storage/views/fs_storage_view.xml new file mode 100644 index 0000000000..d1d892ed99 --- /dev/null +++ b/fs_storage/views/fs_storage_view.xml @@ -0,0 +1,119 @@ + + + + fs.storage.list (in fs_storage) + fs.storage + + + + + + + + + + fs.storage.form (in fs_storage) + fs.storage + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + fs.storage.search (in fs_storage) + fs.storage + + + + + + + + + FS Storage + ir.actions.act_window + fs.storage + list,form + + [] + {} + + + + + form + + + + + + list + + + + +
diff --git a/fs_storage/wizards/__init__.py b/fs_storage/wizards/__init__.py new file mode 100644 index 0000000000..197ee33cb7 --- /dev/null +++ b/fs_storage/wizards/__init__.py @@ -0,0 +1 @@ +from . import fs_test_connection diff --git a/fs_storage/wizards/fs_test_connection.py b/fs_storage/wizards/fs_test_connection.py new file mode 100644 index 0000000000..ebaf6154cc --- /dev/null +++ b/fs_storage/wizards/fs_test_connection.py @@ -0,0 +1,26 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models + + +class FSTestConnection(models.TransientModel): + _name = "fs.test.connection" + _description = "FS Test Connection Wizard" + + def _get_check_connection_method_selection(self): + return self.env["fs.storage"]._get_check_connection_method_selection() + + storage_id = fields.Many2one("fs.storage") + check_connection_method = fields.Selection( + selection="_get_check_connection_method_selection", + required=True, + ) + + @api.model + def default_get(self, field_list): + res = super().default_get(field_list) + res["storage_id"] = self.env.context.get("active_id", False) + return res + + def action_test_config(self): + return self.storage_id._test_config(self.check_connection_method) diff --git a/fs_storage/wizards/fs_test_connection.xml b/fs_storage/wizards/fs_test_connection.xml new file mode 100644 index 0000000000..2846f7e3d3 --- /dev/null +++ b/fs_storage/wizards/fs_test_connection.xml @@ -0,0 +1,30 @@ + + + + fs.test.connection.form + fs.test.connection + +
+ + + + +
+
+
+
+
+ + FS Test Connection + ir.actions.act_window + fs.test.connection + form + new + +
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..0905f9449e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +fsspec>=2024.5.0