oca-storage/odoo-bringout-oca-storage-fs_attachment/fs_attachment/fs_stream.py
2025-08-29 15:43:06 +02:00

108 lines
3.8 KiB
Python

# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from __future__ import annotations
from odoo.http import STATIC_CACHE_LONG, Response, Stream, request
from odoo.tools import config
from .models.ir_attachment import IrAttachment
try:
from werkzeug.utils import secure_filename, send_file as _send_file
except ImportError:
from odoo.tools._vendor.send_file import send_file as _send_file
class FsStream(Stream):
fs_attachment = None
@classmethod
def from_fs_attachment(cls, attachment: IrAttachment) -> FsStream:
attachment.ensure_one()
if not attachment.fs_filename:
raise ValueError("Attachment is not stored into a filesystem storage")
return cls(
mimetype=attachment.mimetype,
download_name=attachment.name,
conditional=True,
etag=attachment.checksum,
type="fs",
size=attachment.file_size,
last_modified=attachment["__last_update"],
fs_attachment=attachment,
)
def read(self):
if self.type == "fs":
with self.fs_attachment.open("rb") as f:
return f.read()
return super().read()
def get_response(
self,
as_attachment=None,
immutable=None,
content_security_policy="default-src 'none'",
**send_file_kwargs,
):
if self.type != "fs":
return super().get_response(
as_attachment=as_attachment, immutable=immutable, **send_file_kwargs
)
if as_attachment is None:
as_attachment = self.as_attachment
if immutable is None:
immutable = self.immutable
# Sanitize the download_name before passing it
safe_download_name = secure_filename(self.download_name or "")
send_file_kwargs = {
"mimetype": self.mimetype,
"as_attachment": as_attachment,
"download_name": safe_download_name,
"conditional": self.conditional,
"etag": self.etag,
"last_modified": self.last_modified,
"max_age": STATIC_CACHE_LONG if immutable else self.max_age,
"environ": request.httprequest.environ,
"response_class": Response,
}
use_x_sendfile = self._fs_use_x_sendfile
# The file will be closed by werkzeug...
send_file_kwargs["use_x_sendfile"] = use_x_sendfile
if not use_x_sendfile:
f = self.fs_attachment.open("rb")
res = _send_file(f, **send_file_kwargs)
else:
x_accel_redirect = (
f"/{self.fs_attachment.fs_storage_code}{self.fs_attachment.fs_url_path}"
)
send_file_kwargs["use_x_sendfile"] = True
res = _send_file("", **send_file_kwargs)
# nginx specific headers
res.headers["X-Accel-Redirect"] = x_accel_redirect
# apache specific headers
res.headers["X-Sendfile"] = x_accel_redirect
res.headers["Content-Length"] = 0
if immutable and res.cache_control:
res.cache_control["immutable"] = None
res.headers["X-Content-Type-Options"] = "nosniff"
if content_security_policy:
res.headers["Content-Security-Policy"] = content_security_policy
return res
@classmethod
def _check_use_x_sendfile(cls, attachment: IrAttachment) -> bool:
return (
config["x_sendfile"]
and attachment.fs_url
and attachment.fs_storage_id.use_x_sendfile_to_serve_internal_url
)
@property
def _fs_use_x_sendfile(self) -> bool:
"""Return True if x-sendfile should be used to serve the file"""
return self._check_use_x_sendfile(self.fs_attachment)