diff --git a/fs_attachment_azure/README.rst b/fs_attachment_azure/README.rst new file mode 100644 index 0000000000..8c3bd487b5 --- /dev/null +++ b/fs_attachment_azure/README.rst @@ -0,0 +1,175 @@ +=================== +Fs Attachment Azure +=================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:c01d32f225802fc30d7d79a6130c073016c0d98c69ba20dd6d6c0d1213e9f92d + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github + :target: https://github.com/OCA/storage/tree/17.0/fs_attachment_azure + :alt: OCA/storage +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/storage-17-0/storage-17-0-fs_attachment_azure + :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=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of +`fs_attachment `__ +to better support Azure storage. It includes features such as: + +- Special handling of X-Accel-Redirect headers for Azure storages. +- Options for using signed URLs in X-Accel-Redirect. (This is required + to be able to serve files from a private Azure Blob Storage using + X-Accel-Redirect without exposing the files publicly.) + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +On the Odoo instance, go to *Settings* > *Technical* > *Storage* > *File +Storage*. + +When you create a new storage for Azure or modify an existing one, when +you activate the option "Use X-Sendfile To Serve Internal Url", 2 +additional fields will appear: + +- **Azure Uses Signed URL For X-Accel-Redirect**: If checked, the + X-Accel-Redirect path will be a signed URL, which is useful for S3 + storages that require signed URLs for access. +- **Azure Signed URL Expiration**: The expiration time for the signed + URL in seconds. This field is only relevant if the previous option is + checked. By default, it is set to 30 seconds but it could be less + since the url generated into the X-Accel-Redirect process is directly + used by the web server to serve the file. + +The value of these fields can also be set in the server environment +variables using the keys: + +- *azure_uses_signed_url_for_x_sendfile* +- *azure_signed_url_expiration* + +When the option "Use X-Sendfile To Serve Internal Url" is enabled, the +system will generate an X-Accel-Redirect header in the response to a +request to get a file. In the case of Azure storages, it will follow the +format: + +.. code:: text + + X-Accel-Redirect: /fs_x_sendfile/{scheme}/{host}/{path with query if any} + +Where: + +- ``{scheme}``: The URL scheme (http or https). +- ``{host}``: The host of the Azure storage. +- ``{path with query if any}``: The path to the file in the Azure + storage, including any query parameters. (Query parameters are set + when the ``azure_uses_signed_url_for_x_sendfile`` option is enabled.) + +In order to serve files using X-Accel-Redirect, you must ensure that +your web server is configured to handle these headers correctly. This +typically involves setting up a location block in your web server +configuration that matches the X-Accel-Redirect path and proxies the +request to the Azure storage. + +For example, if you are using Nginx, you would add a location block like +this: + +.. code:: nginx + + + location ~ ^/fs_x_sendfile/(.*?)/(.*?)/(.*) { + internal; + set $url_scheme $1; + set $url_host $2; + set $url_path $3; + set $url $url_scheme://$url_host/$url_path; + + proxy_pass $url$is_args$args; + proxy_set_header Host $url_host; + proxy_ssl_server_name on; + + } + +Unlike the standard implementation of X-Accel-Redirect on non Azure +storages, the Azure implementation does not require a base URL to be set +in the storage configuration. The X-Accel-Redirect path is constructed +directly from the Azure storage's URL defined for the connection, the +directory name as bucket name, and the file path. + +Changelog +========= + +17.0.1.0.0 (2026-07-13) +----------------------- + +- This module was "forked" from fs_attachment_s3 v17.0.1.2.1 + +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 +* Camptocamp + +Contributors +------------ + +- Laurent Mignon laurent.mignon@acsone.eu (https://www.acsone.eu) +- Stéphane Bidoul stephane.bidoul@acsone.eu (https://www.acsone.eu) +- Akim Juillerat akim.juillerat@camptocamp.com + +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. + +.. |maintainer-grindtildeath| image:: https://github.com/grindtildeath.png?size=40px + :target: https://github.com/grindtildeath + :alt: grindtildeath + +Current `maintainer `__: + +|maintainer-grindtildeath| + +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_attachment_azure/__init__.py b/fs_attachment_azure/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/fs_attachment_azure/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/fs_attachment_azure/__manifest__.py b/fs_attachment_azure/__manifest__.py new file mode 100644 index 0000000000..8244fc6974 --- /dev/null +++ b/fs_attachment_azure/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2025 ACSONE SA/NV +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Fs Attachment Azure", + "summary": """Store attachments into Azure Blob storage""", + "version": "17.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Camptocamp,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/storage", + "depends": ["fs_attachment"], + "external_dependencies": { + "python": [ + "adlfs", + ], + }, + "data": [ + "views/fs_storage.xml", + ], + "maintainers": ["grindtildeath"], +} diff --git a/fs_attachment_azure/models/__init__.py b/fs_attachment_azure/models/__init__.py new file mode 100644 index 0000000000..45a28cbdca --- /dev/null +++ b/fs_attachment_azure/models/__init__.py @@ -0,0 +1,2 @@ +from . import fs_storage +from . import ir_attachment diff --git a/fs_attachment_azure/models/fs_storage.py b/fs_attachment_azure/models/fs_storage.py new file mode 100644 index 0000000000..31c0acb2d7 --- /dev/null +++ b/fs_attachment_azure/models/fs_storage.py @@ -0,0 +1,56 @@ +# Copyright 2025 ACSONE SA/NV +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import fsspec.asyn + +from odoo import api, fields, models + + +class FsStorage(models.Model): + _inherit = "fs.storage" + + azure_uses_signed_url_for_x_sendfile = fields.Boolean( + string="Use signed URL for X-Accel-Redirect", + help="If checked, the storage will use signed URLs for attachments " + "when using X-Accel-Redirect. This is useful for Azure storage where the " + "file path is not directly accessible without authentication.", + ) + azure_signed_url_expiration = fields.Integer( + string="Signed URL Expiration (seconds)", + default=30, + help="The expiration time for the signed URL in seconds. " + "Default is 30 seconds.", + ) + + @property + def _server_env_fields(self): + """Override to include Azure specific fields.""" + fields = super()._server_env_fields + fields.update( + { + "azure_uses_signed_url_for_x_sendfile": {}, + "azure_signed_url_expiration": {}, + } + ) + return fields + + @property + def is_azure_storage(self): + """Check if the storage is an Azure storage.""" + self.ensure_one() + fs = self._get_root_filesystem(self.fs) + protocol = getattr(fs, "protocol", []) + return self.protocol in protocol + + @api.model + def _azure_call_synchronous(self, azure_client_function, *args, **kwargs): + # adlfs uses asynchronous client + # We need to run the async function in a synchronous context. + return fsspec.asyn.sync( + fsspec.asyn.get_loop(), + azure_client_function, + *args, + timeout=None, + **kwargs, + ) diff --git a/fs_attachment_azure/models/ir_attachment.py b/fs_attachment_azure/models/ir_attachment.py new file mode 100644 index 0000000000..c171bae118 --- /dev/null +++ b/fs_attachment_azure/models/ir_attachment.py @@ -0,0 +1,103 @@ +# Copyright 2025 ACSONE SA/NV +# Copyright 2025 XCG SAS +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import datetime +from urllib.parse import urlparse + +from adlfs.spec import BlobSasPermissions, generate_blob_sas + +from odoo import models + + +class IrAttachment(models.Model): + _inherit = "ir.attachment" + + def _get_x_sendfile_path(self): + self.ensure_one() + storage = self.fs_storage_id + if storage.is_azure_storage: + return self._get_azure_x_sendfile_path() + return super()._get_x_sendfile_path() + + def _fs_use_x_sendfile(self): + self.ensure_one() + storage = self.fs_storage_id + if storage.is_azure_storage: + return storage.use_x_sendfile_to_serve_internal_url + return super()._fs_use_x_sendfile() + + def _get_azure_x_sendfile_path(self): + """Generate the X-Accel-Redirect path for Azure storage. + + This method is used to generate the path for Azure storage when using + X-Accel-Redirect. It constructs the path based on the Azure container and + file path, ensuring that it is compatible with the Azure storage + configuration and the Odoo file storage system. + + Args: + attachment (IrAttachment): The attachment record for which the + X-Accel-Redirect path is being generated. + Returns: + str: The X-Accel-Redirect path for the Azure storage. + + The path is formatted as: + /fs_x_sendfile/// + + where: + - `` is the scheme of the base URL (e.g., 'https'). + - `` is the netloc of the base URL + (e.g., 'myaccount.blob.core.windows.net'). + - `` is the path to the file in the Azure container, including the + container name + """ + fs, storage_code, file_path = self._get_fs_parts() + storage = self.env["fs.storage"].sudo().get_by_code(storage_code) + root_fs = storage._get_root_filesystem(fs) + azure_client = root_fs.service_client + container_name = storage.get_directory_path() + blob_client = azure_client.get_blob_client(container_name, file_path) + if storage.azure_uses_signed_url_for_x_sendfile: + if ( + azure_client.connection_string + or azure_client.account_name + and azure_client.account_key + ): + file_url = azure_client.url( + file_path, expires=storage.azure_signed_url_expiration + ) + else: + # Ideally we would be able to call azure_client.url() as it is calling + # generate_blob_sas. However, it expects to use an account shared key + # (i.e either a connection string or account name/key pair). + # For this we need to get a delegation key first + now = datetime.datetime.now() + expiry_time = now + datetime.timedelta( + seconds=storage.azure_signed_url_expiration + ) + delegation_key = storage._azure_call_synchronous( + azure_client.get_user_delegation_key, + key_start_time=now, + key_expiry_time=expiry_time, + ) + # Then we can call generate_blob_sas + sas_token = storage._azure_call_synchronous( + generate_blob_sas, + fs.fs.account_name, + fs.path, + file_path, + user_delegation_key=delegation_key, + permission=BlobSasPermissions(read=True), + expiry=expiry_time, + ) + file_url = f"{blob_client.url}?{sas_token}" + else: + file_url = blob_client.url + + parsed_url = urlparse(file_url) + path = parsed_url.path.strip("/") + query = parsed_url.query + redirect_path = f"/fs_x_sendfile/{parsed_url.scheme}/{parsed_url.netloc}/{path}" + if query: + redirect_path += f"?{query}" + return redirect_path diff --git a/fs_attachment_azure/pyproject.toml b/fs_attachment_azure/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/fs_attachment_azure/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fs_attachment_azure/readme/CONFIGURE.md b/fs_attachment_azure/readme/CONFIGURE.md new file mode 100644 index 0000000000..84842eef89 --- /dev/null +++ b/fs_attachment_azure/readme/CONFIGURE.md @@ -0,0 +1,64 @@ +On the Odoo instance, go to *Settings* > *Technical* > *Storage* > *File Storage*. + +When you create a new storage for Azure or modify an existing one, when you activate +the option "Use X-Sendfile To Serve Internal Url", 2 additional fields will appear: + +- **Azure Uses Signed URL For X-Accel-Redirect**: If checked, the X-Accel-Redirect + path will be a signed URL, which is useful for S3 storages that require + signed URLs for access. +- **Azure Signed URL Expiration**: The expiration time for the signed URL in seconds. + This field is only relevant if the previous option is checked. By default, + it is set to 30 seconds but it could be less since the url generated into + the X-Accel-Redirect process is directly used by the web server to serve the file. + +The value of these fields can also be set in the server environment variables using +the keys: + +- *azure_uses_signed_url_for_x_sendfile* +- *azure_signed_url_expiration* + +When the option "Use X-Sendfile To Serve Internal Url" is enabled, the system will +generate an X-Accel-Redirect header in the response to a request to get a file. +In the case of Azure storages, it will follow the format: + +```text +X-Accel-Redirect: /fs_x_sendfile/{scheme}/{host}/{path with query if any} +``` + +Where: + +- `{scheme}`: The URL scheme (http or https). +- `{host}`: The host of the Azure storage. +- `{path with query if any}`: The path to the file in the Azure storage, + including any query parameters. (Query parameters are set when the + `azure_uses_signed_url_for_x_sendfile` option is enabled.) + +In order to serve files using X-Accel-Redirect, you must ensure that your +web server is configured to handle these headers correctly. This typically +involves setting up a location block in your web server configuration that +matches the X-Accel-Redirect path and proxies the request to the Azure storage. + +For example, if you are using Nginx, you would add a location block like this: + +```nginx + + location ~ ^/fs_x_sendfile/(.*?)/(.*?)/(.*) { + internal; + set $url_scheme $1; + set $url_host $2; + set $url_path $3; + set $url $url_scheme://$url_host/$url_path; + + proxy_pass $url$is_args$args; + proxy_set_header Host $url_host; + proxy_ssl_server_name on; + + } +``` + + +Unlike the standard implementation of X-Accel-Redirect on non Azure storages, +the Azure implementation does not require a base URL to be set in the storage +configuration. The X-Accel-Redirect path is constructed directly from the +Azure storage's URL defined for the connection, the directory name as +bucket name, and the file path. diff --git a/fs_attachment_azure/readme/CONTRIBUTORS.md b/fs_attachment_azure/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..cb7b22f28c --- /dev/null +++ b/fs_attachment_azure/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Laurent Mignon (https://www.acsone.eu) +- Stéphane Bidoul (https://www.acsone.eu) +- Akim Juillerat \ No newline at end of file diff --git a/fs_attachment_azure/readme/DESCRIPTION.md b/fs_attachment_azure/readme/DESCRIPTION.md new file mode 100644 index 0000000000..e21f93c98c --- /dev/null +++ b/fs_attachment_azure/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +This module extends the functionality of [fs_attachment](https://github.com/OCA/storage/tree/16.0/fs_attachment) +to better support Azure storage. It includes features such as: + +- Special handling of X-Accel-Redirect headers for Azure storages. +- Options for using signed URLs in X-Accel-Redirect. (This is required to be able to serve files from a private Azure Blob Storage + using X-Accel-Redirect without exposing the files publicly.) diff --git a/fs_attachment_azure/readme/HISTORY.md b/fs_attachment_azure/readme/HISTORY.md new file mode 100644 index 0000000000..9ec3de838e --- /dev/null +++ b/fs_attachment_azure/readme/HISTORY.md @@ -0,0 +1,3 @@ +## 17.0.1.0.0 (2026-07-13) + +- This module was "forked" from fs_attachment_s3 v17.0.1.2.1 diff --git a/fs_attachment_azure/readme/newsfragments/.gitkeep b/fs_attachment_azure/readme/newsfragments/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/fs_attachment_azure/static/description/icon.png b/fs_attachment_azure/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/fs_attachment_azure/static/description/icon.png differ diff --git a/fs_attachment_azure/static/description/index.html b/fs_attachment_azure/static/description/index.html new file mode 100644 index 0000000000..ee564e4e01 --- /dev/null +++ b/fs_attachment_azure/static/description/index.html @@ -0,0 +1,515 @@ + + + + + +Fs Attachment Azure + + + +
+

Fs Attachment Azure

+ + +

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

+

This module extends the functionality of +fs_attachment +to better support Azure storage. It includes features such as:

+
    +
  • Special handling of X-Accel-Redirect headers for Azure storages.
  • +
  • Options for using signed URLs in X-Accel-Redirect. (This is required +to be able to serve files from a private Azure Blob Storage using +X-Accel-Redirect without exposing the files publicly.)
  • +
+

Table of contents

+ +
+

Configuration

+

On the Odoo instance, go to Settings > Technical > Storage > File +Storage.

+

When you create a new storage for Azure or modify an existing one, when +you activate the option “Use X-Sendfile To Serve Internal Url”, 2 +additional fields will appear:

+
    +
  • Azure Uses Signed URL For X-Accel-Redirect: If checked, the +X-Accel-Redirect path will be a signed URL, which is useful for S3 +storages that require signed URLs for access.
  • +
  • Azure Signed URL Expiration: The expiration time for the signed +URL in seconds. This field is only relevant if the previous option is +checked. By default, it is set to 30 seconds but it could be less +since the url generated into the X-Accel-Redirect process is directly +used by the web server to serve the file.
  • +
+

The value of these fields can also be set in the server environment +variables using the keys:

+
    +
  • azure_uses_signed_url_for_x_sendfile
  • +
  • azure_signed_url_expiration
  • +
+

When the option “Use X-Sendfile To Serve Internal Url” is enabled, the +system will generate an X-Accel-Redirect header in the response to a +request to get a file. In the case of Azure storages, it will follow the +format:

+
+X-Accel-Redirect: /fs_x_sendfile/{scheme}/{host}/{path with query if any}
+
+

Where:

+
    +
  • {scheme}: The URL scheme (http or https).
  • +
  • {host}: The host of the Azure storage.
  • +
  • {path with query if any}: The path to the file in the Azure +storage, including any query parameters. (Query parameters are set +when the azure_uses_signed_url_for_x_sendfile option is enabled.)
  • +
+

In order to serve files using X-Accel-Redirect, you must ensure that +your web server is configured to handle these headers correctly. This +typically involves setting up a location block in your web server +configuration that matches the X-Accel-Redirect path and proxies the +request to the Azure storage.

+

For example, if you are using Nginx, you would add a location block like +this:

+
+location ~ ^/fs_x_sendfile/(.*?)/(.*?)/(.*) {
+    internal;
+    set $url_scheme $1;
+    set $url_host $2;
+    set $url_path $3;
+    set $url $url_scheme://$url_host/$url_path;
+
+    proxy_pass $url$is_args$args;
+    proxy_set_header Host $url_host;
+    proxy_ssl_server_name on;
+
+}
+
+

Unlike the standard implementation of X-Accel-Redirect on non Azure +storages, the Azure implementation does not require a base URL to be set +in the storage configuration. The X-Accel-Redirect path is constructed +directly from the Azure storage’s URL defined for the connection, the +directory name as bucket name, and the file path.

+
+
+

Changelog

+
+

17.0.1.0.0 (2026-07-13)

+
    +
  • This module was “forked” from fs_attachment_s3 v17.0.1.2.1
  • +
+
+
+
+

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
  • +
  • Camptocamp
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainer:

+

grindtildeath

+

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_attachment_azure/tests/__init__.py b/fs_attachment_azure/tests/__init__.py new file mode 100644 index 0000000000..fe85fc1d1c --- /dev/null +++ b/fs_attachment_azure/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fs_attachment_azure diff --git a/fs_attachment_azure/tests/common.py b/fs_attachment_azure/tests/common.py new file mode 100644 index 0000000000..6d7c12454f --- /dev/null +++ b/fs_attachment_azure/tests/common.py @@ -0,0 +1,47 @@ +# Copyright 2025 ACSONE SA/NV (http://acsone.eu). +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.tests.common import TransactionCase + +from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT + + +class TestFSAttachmentAzureCommon(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, **DISABLED_MAIL_CONTEXT)) + cls.azure_backend_config = { + "name": "Azure Storage", + "protocol": "az", + "code": "azure", + "directory_path": "test-blob", + } + cls.azure_backend = cls.env["fs.storage"].create(cls.azure_backend_config) + cls.ir_attachment_model = cls.env["ir.attachment"] + + cls.fake_attachment_azure = cls.env["ir.attachment"].create( + { + "name": "fake_azure_file.txt", + "fs_storage_id": cls.azure_backend.id, + } + ) + cls.fake_attachment_azure.flush_recordset() + # update the attachment into database since we don't have a real blob storage + cls.env.cr.execute( + """ + UPDATE + ir_attachment + SET + store_fname = 'azure://dir/sub/fake_azure_file.txt', + fs_filename = 'fake_azure_file.txt', + fs_storage_code = 'azure', + checksum = 234, + file_size = 1234, + fs_storage_id = %s + WHERE + id = %s + """, + (cls.azure_backend.id, cls.fake_attachment_azure.id), + ) + cls.fake_attachment_azure.invalidate_recordset() diff --git a/fs_attachment_azure/tests/test_fs_attachment_azure.py b/fs_attachment_azure/tests/test_fs_attachment_azure.py new file mode 100644 index 0000000000..5c715c4018 --- /dev/null +++ b/fs_attachment_azure/tests/test_fs_attachment_azure.py @@ -0,0 +1,62 @@ +# Copyright 2025 ACSONE SA/NV (http://acsone.eu). +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from unittest.mock import AsyncMock, MagicMock, Mock, patch + +from adlfs import AzureBlobFileSystem + +from .common import TestFSAttachmentAzureCommon + +PROTOCOL = "https" +ACCOUNT_NAME = "myaccount" +ACCOUNT_KEY = "123456789" +DOMAIN = "blob.core.windows.net" +CONTAINER = "test-blob" +PATH = "dir/sub" +FILENAME = "fake_azure_file.txt" +BASE_URL = f"{PROTOCOL}://{ACCOUNT_NAME}.{DOMAIN}/{CONTAINER}/{PATH}/{FILENAME}" +TOKEN = "1111-2222-3333-4444" +CONNECTION_STRING = f"DefaultEndpointsProtocol={PROTOCOL};AccountName={ACCOUNT_NAME};AccountKey={ACCOUNT_KEY};BlobEndpoint={PROTOCOL}://{DOMAIN}/{ACCOUNT_NAME};" + + +def _fake_do_connect(self): + mock_service_client = MagicMock() + mock_blob_client = MagicMock() + mock_blob_client.url = BASE_URL + mock_service_client.get_container_client.return_value = Mock() + mock_service_client.get_blob_client.return_value = mock_blob_client + mock_service_client.connection_string = CONNECTION_STRING + mock_service_client.url.return_value = "?".join([BASE_URL, TOKEN]) + mock_service_client.close = AsyncMock(return_value="ok") + self.service_client = mock_service_client + + +class TestFSAttachementAzure(TestFSAttachmentAzureCommon): + def test_get_x_sendfile_path_azure_signed(self): + """Test the X-Accel-Redirect path generation for azure storage.""" + self.azure_backend.write( + { + "azure_uses_signed_url_for_x_sendfile": True, + "azure_signed_url_expiration": 60, + } + ) + with patch.object(AzureBlobFileSystem, "do_connect", _fake_do_connect): + url = self.fake_attachment_azure._get_x_sendfile_path() + self.assertTrue( + url.startswith( + "/fs_x_sendfile/https/myaccount.blob.core.windows.net/test-blob/dir/sub/fake_azure_file.txt?1111-2222-3333-4444" + ), + "The end of the path should contain the path to the file " + f"name and query parameters. ({url})", + ) + + def test_get_x_sendfile_path_azure(self): + """Test the X-Accel-Redirect path generation.""" + with patch.object(AzureBlobFileSystem, "do_connect", _fake_do_connect): + url = self.fake_attachment_azure._get_x_sendfile_path() + + self.assertEqual( + url, + "/fs_x_sendfile/https/myaccount.blob.core.windows.net/test-blob/dir/sub/fake_azure_file.txt", + f"The X-Accel-Redirect path should match the expected format. ({url})", + ) diff --git a/fs_attachment_azure/views/fs_storage.xml b/fs_attachment_azure/views/fs_storage.xml new file mode 100644 index 0000000000..d1265a3884 --- /dev/null +++ b/fs_attachment_azure/views/fs_storage.xml @@ -0,0 +1,24 @@ + + + + + + fs.storage + + + + + + + + + + diff --git a/requirements.txt b/requirements.txt index 275c9dfb40..0bc35526f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ # generated from manifests external_dependencies +adlfs fsspec>=2024.5.0 fsspec>=2025.3.0 fsspec[s3]