Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions fs_attachment_azure/README.rst
Original file line number Diff line number Diff line change
@@ -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 <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.)

**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 <https://github.com/OCA/storage/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 <https://github.com/OCA/storage/issues/new?body=module:%20fs_attachment_azure%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

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 <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-grindtildeath|

This module is part of the `OCA/storage <https://github.com/OCA/storage/tree/17.0/fs_attachment_azure>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions fs_attachment_azure/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
22 changes: 22 additions & 0 deletions fs_attachment_azure/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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"],
}
2 changes: 2 additions & 0 deletions fs_attachment_azure/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import fs_storage
from . import ir_attachment
56 changes: 56 additions & 0 deletions fs_attachment_azure/models/fs_storage.py
Original file line number Diff line number Diff line change
@@ -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,
)
103 changes: 103 additions & 0 deletions fs_attachment_azure/models/ir_attachment.py
Original file line number Diff line number Diff line change
@@ -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/<scheme>/<netloc>/<path>

where:
- `<scheme>` is the scheme of the base URL (e.g., 'https').
- `<netloc>` is the netloc of the base URL
(e.g., 'myaccount.blob.core.windows.net').
- `<path>` 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
3 changes: 3 additions & 0 deletions fs_attachment_azure/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
Loading
Loading