mirror of
https://github.com/bringout/oca-storage.git
synced 2026-04-19 11:51:59 +02:00
Initial commit: OCA Storage packages (17 packages)
This commit is contained in:
commit
7a380f05d3
659 changed files with 41828 additions and 0 deletions
|
|
@ -0,0 +1,3 @@
|
|||
from . import storage_file
|
||||
from . import storage_backend
|
||||
from . import ir_actions_report
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# Copyright 2021 Camptocamp SA (http://www.camptocamp.com).
|
||||
# @author Simone Orsi <simahawk@gmail.com>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrActionsReport(models.Model):
|
||||
_inherit = "ir.actions.report"
|
||||
|
||||
def render_qweb_pdf(self, res_ids=None, data=None):
|
||||
return super(
|
||||
IrActionsReport, self.with_context(print_report_pdf=True)
|
||||
).render_qweb_pdf(res_ids=res_ids, data=data)
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
# Copyright 2017 Akretion (http://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# Copyright 2019 Camptocamp SA (http://www.camptocamp.com).
|
||||
# @author Simone Orsi <simone.orsi@camptocamp.com>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StorageBackend(models.Model):
|
||||
_inherit = "storage.backend"
|
||||
|
||||
filename_strategy = fields.Selection(
|
||||
selection=[("name_with_id", "Name and ID"), ("hash", "SHA hash")],
|
||||
default="name_with_id",
|
||||
help=(
|
||||
"Strategy to build the name of the file to be stored.\n"
|
||||
"Name and ID: will store the file with its name + its id.\n"
|
||||
"SHA Hash: will use the hash of the file as filename "
|
||||
"(same method as the native attachment storage)"
|
||||
),
|
||||
)
|
||||
served_by = fields.Selection(
|
||||
selection=[("odoo", "Odoo"), ("external", "External")],
|
||||
required=True,
|
||||
default="odoo",
|
||||
)
|
||||
base_url = fields.Char(default="")
|
||||
is_public = fields.Boolean(
|
||||
default=False,
|
||||
help="Define if every files stored into this backend are "
|
||||
"public or not. Examples:\n"
|
||||
"Private: your file/image can not be displayed is the user is "
|
||||
"not logged (not available on other website);\n"
|
||||
"Public: your file/image can be displayed if nobody is "
|
||||
"logged (useful to display files on external websites)",
|
||||
)
|
||||
url_include_directory_path = fields.Boolean(
|
||||
default=False,
|
||||
help="Normally the directory_path it's for internal usage. "
|
||||
"If this flag is enabled "
|
||||
"the path will be used to compute the public URL.",
|
||||
)
|
||||
base_url_for_files = fields.Char(compute="_compute_base_url_for_files", store=True)
|
||||
backend_view_use_internal_url = fields.Boolean(
|
||||
help="Decide if Odoo backend views should use the external URL (usually a CDN) "
|
||||
"or the internal url with direct access to the storage. "
|
||||
"This could save you some money if you pay by CDN traffic."
|
||||
)
|
||||
|
||||
def write(self, vals):
|
||||
# Ensure storage file URLs are up to date
|
||||
clear_url_cache = False
|
||||
url_related_fields = (
|
||||
"served_by",
|
||||
"base_url",
|
||||
"directory_path",
|
||||
"url_include_directory_path",
|
||||
)
|
||||
for fname in url_related_fields:
|
||||
if fname in vals:
|
||||
clear_url_cache = True
|
||||
break
|
||||
res = super().write(vals)
|
||||
if clear_url_cache:
|
||||
self.action_recompute_base_url_for_files()
|
||||
return res
|
||||
|
||||
@property
|
||||
def _server_env_fields(self):
|
||||
env_fields = super()._server_env_fields
|
||||
env_fields.update(
|
||||
{
|
||||
"filename_strategy": {},
|
||||
"served_by": {},
|
||||
"base_url": {},
|
||||
"url_include_directory_path": {},
|
||||
}
|
||||
)
|
||||
return env_fields
|
||||
|
||||
_default_backend_xid = "storage_backend.default_storage_backend"
|
||||
|
||||
@classmethod
|
||||
def _get_backend_id_from_param(cls, env, param_name, default_fallback=True):
|
||||
backend_id = None
|
||||
param = env["ir.config_parameter"].sudo().get_param(param_name)
|
||||
if param:
|
||||
if param.isdigit():
|
||||
backend_id = int(param)
|
||||
elif "." in param:
|
||||
backend = env.ref(param, raise_if_not_found=False)
|
||||
if backend:
|
||||
backend_id = backend.id
|
||||
if not backend_id and default_fallback:
|
||||
backend = env.ref(cls._default_backend_xid, raise_if_not_found=False)
|
||||
if backend:
|
||||
backend_id = backend.id
|
||||
else:
|
||||
_logger.warn("No backend found, no default fallback found.")
|
||||
return backend_id
|
||||
|
||||
@api.depends(
|
||||
"served_by",
|
||||
"base_url",
|
||||
"directory_path",
|
||||
"url_include_directory_path",
|
||||
)
|
||||
def _compute_base_url_for_files(self):
|
||||
for record in self:
|
||||
record.base_url_for_files = record._get_base_url_for_files()
|
||||
|
||||
def _get_base_url_for_files(self):
|
||||
"""Retrieve base URL for files."""
|
||||
backend = self.sudo()
|
||||
parts = []
|
||||
if backend.served_by == "external":
|
||||
parts = [backend.base_url or ""]
|
||||
if backend.url_include_directory_path and backend.directory_path:
|
||||
parts.append(backend.directory_path)
|
||||
return "/".join(parts)
|
||||
|
||||
def action_recompute_base_url_for_files(self):
|
||||
"""Refresh base URL for files.
|
||||
|
||||
Rationale: all the params for computing this URL might come from server env.
|
||||
When this is the case, the URL - being stored - might be out of date.
|
||||
This is because changes to server env fields are not detected at startup.
|
||||
Hence, let's offer an easy way to promptly force this manually when needed.
|
||||
"""
|
||||
self._compute_base_url_for_files()
|
||||
self.env["storage.file"].invalidate_model(["url"])
|
||||
|
||||
def _get_base_url_from_param(self):
|
||||
base_url_param = (
|
||||
"report.url" if self.env.context.get("print_report_pdf") else "web.base.url"
|
||||
)
|
||||
return self.env["ir.config_parameter"].sudo().get_param(base_url_param)
|
||||
|
||||
def _get_url_for_file(self, storage_file):
|
||||
"""Return final full URL for given file."""
|
||||
backend = self.sudo()
|
||||
if backend.served_by == "odoo":
|
||||
parts = [
|
||||
self._get_base_url_from_param(),
|
||||
"storage.file",
|
||||
storage_file.slug,
|
||||
]
|
||||
else:
|
||||
parts = [backend.base_url_for_files or "", storage_file.relative_path or ""]
|
||||
return "/".join([x.rstrip("/") for x in parts if x])
|
||||
|
||||
def _register_hook(self):
|
||||
super()._register_hook()
|
||||
backends = self.search([]).filtered(
|
||||
lambda x: x._get_base_url_for_files() != x.base_url_for_files
|
||||
)
|
||||
if not backends:
|
||||
return
|
||||
sql = f"SELECT id FROM {self._table} WHERE ID IN %s FOR UPDATE"
|
||||
self.env.cr.execute(sql, (tuple(backends.ids),), log_exceptions=False)
|
||||
backends.action_recompute_base_url_for_files()
|
||||
_logger.info("storage.backend base URL for files refreshed")
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
# Copyright 2017 Akretion (http://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import human_size
|
||||
from odoo.tools.translate import _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from slugify import slugify
|
||||
except ImportError: # pragma: no cover
|
||||
_logger.debug("Cannot `import slugify`.")
|
||||
|
||||
|
||||
REGEX_SLUGIFY = r"[^-a-z0-9_]+"
|
||||
|
||||
|
||||
class StorageFile(models.Model):
|
||||
_name = "storage.file"
|
||||
_description = "Storage File"
|
||||
|
||||
name = fields.Char(required=True, index=True)
|
||||
backend_id = fields.Many2one(
|
||||
"storage.backend", "Storage", index=True, required=True
|
||||
)
|
||||
url = fields.Char(compute="_compute_url", help="HTTP accessible path to the file")
|
||||
internal_url = fields.Char(
|
||||
compute="_compute_internal_url",
|
||||
help="HTTP URL to load the file directly from storage.",
|
||||
)
|
||||
slug = fields.Char(
|
||||
compute="_compute_slug", help="Slug-ified name with ID for URL", store=True
|
||||
)
|
||||
relative_path = fields.Char(readonly=True, help="Relative location for backend")
|
||||
file_size = fields.Integer()
|
||||
human_file_size = fields.Char(compute="_compute_human_file_size", store=True)
|
||||
checksum = fields.Char("Checksum/SHA1", size=40, index=True, readonly=True)
|
||||
filename = fields.Char(
|
||||
"Filename without extension", compute="_compute_extract_filename", store=True
|
||||
)
|
||||
extension = fields.Char(compute="_compute_extract_filename", store=True)
|
||||
mimetype = fields.Char("Mime Type", compute="_compute_extract_filename", store=True)
|
||||
data = fields.Binary(
|
||||
help="Datas", inverse="_inverse_data", compute="_compute_data", store=False
|
||||
)
|
||||
to_delete = fields.Boolean()
|
||||
active = fields.Boolean(default=True)
|
||||
company_id = fields.Many2one(
|
||||
"res.company", "Company", default=lambda self: self.env.user.company_id.id
|
||||
)
|
||||
file_type = fields.Selection([])
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"path_uniq",
|
||||
"unique(relative_path, backend_id)",
|
||||
"The private path must be uniq per backend",
|
||||
)
|
||||
]
|
||||
|
||||
def write(self, vals):
|
||||
if "data" in vals:
|
||||
for record in self:
|
||||
if record.data:
|
||||
raise UserError(
|
||||
_("File can not be updated, remove it and create a new one")
|
||||
)
|
||||
return super(StorageFile, self).write(vals)
|
||||
|
||||
@api.depends("file_size")
|
||||
def _compute_human_file_size(self):
|
||||
for record in self:
|
||||
record.human_file_size = human_size(record.file_size)
|
||||
|
||||
@api.depends("filename", "extension")
|
||||
def _compute_slug(self):
|
||||
for record in self:
|
||||
record.slug = record._slugify_name_with_id()
|
||||
|
||||
def _slugify_name_with_id(self):
|
||||
return "{}{}".format(
|
||||
slugify(
|
||||
"{}-{}".format(self.filename, self.id), regex_pattern=REGEX_SLUGIFY
|
||||
),
|
||||
self.extension,
|
||||
)
|
||||
|
||||
def _build_relative_path(self, checksum):
|
||||
self.ensure_one()
|
||||
strategy = self.sudo().backend_id.filename_strategy
|
||||
if not strategy:
|
||||
raise UserError(
|
||||
_(
|
||||
"The filename strategy is empty for the backend %s.\n"
|
||||
"Please configure it"
|
||||
)
|
||||
% self.backend_id.name
|
||||
)
|
||||
if strategy == "hash":
|
||||
return checksum[:2] + "/" + checksum
|
||||
elif strategy == "name_with_id":
|
||||
return self.slug
|
||||
|
||||
def _prepare_meta_for_file(self):
|
||||
bin_data = base64.b64decode(self.data)
|
||||
checksum = hashlib.sha1(bin_data).hexdigest()
|
||||
relative_path = self._build_relative_path(checksum)
|
||||
return {
|
||||
"checksum": checksum,
|
||||
"file_size": len(bin_data),
|
||||
"relative_path": relative_path,
|
||||
}
|
||||
|
||||
def _inverse_data(self):
|
||||
for record in self:
|
||||
record.write(record._prepare_meta_for_file())
|
||||
record.backend_id.sudo().add(
|
||||
record.relative_path,
|
||||
record.data,
|
||||
mimetype=record.mimetype,
|
||||
binary=False,
|
||||
)
|
||||
|
||||
def _compute_data(self):
|
||||
for rec in self:
|
||||
if self._context.get("bin_size"):
|
||||
rec.data = rec.file_size
|
||||
elif rec.relative_path:
|
||||
rec.data = rec.backend_id.sudo().get(rec.relative_path, binary=False)
|
||||
else:
|
||||
rec.data = None
|
||||
|
||||
@api.depends("relative_path", "backend_id")
|
||||
def _compute_url(self):
|
||||
for record in self:
|
||||
record.url = record._get_url()
|
||||
|
||||
def _get_url(self):
|
||||
"""Retrieve file URL based on backend params."""
|
||||
return self.backend_id._get_url_for_file(self)
|
||||
|
||||
@api.depends("slug")
|
||||
def _compute_internal_url(self):
|
||||
for record in self:
|
||||
record.internal_url = record._get_internal_url()
|
||||
|
||||
def _get_internal_url(self):
|
||||
"""Retrieve file URL to load file directly from the storage.
|
||||
|
||||
It is recommended to use this for Odoo backend internal usage
|
||||
to not generate traffic on external services.
|
||||
"""
|
||||
return f"/storage.file/{self.slug}"
|
||||
|
||||
@api.depends("name")
|
||||
def _compute_extract_filename(self):
|
||||
for rec in self:
|
||||
if rec.name:
|
||||
rec.filename, rec.extension = os.path.splitext(rec.name)
|
||||
mime, __ = mimetypes.guess_type(rec.name)
|
||||
else:
|
||||
rec.filename = rec.extension = mime = False
|
||||
rec.mimetype = mime
|
||||
|
||||
def unlink(self):
|
||||
if self._context.get("cleanning_storage_file"):
|
||||
super(StorageFile, self).unlink()
|
||||
else:
|
||||
self.write({"to_delete": True, "active": False})
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def _clean_storage_file(self):
|
||||
# we must be sure that all the changes are into the DB since
|
||||
# we by pass the ORM
|
||||
self.flush_model()
|
||||
self._cr.execute(
|
||||
"""SELECT id
|
||||
FROM storage_file
|
||||
WHERE to_delete=True FOR UPDATE"""
|
||||
)
|
||||
ids = [x[0] for x in self._cr.fetchall()]
|
||||
for st_file in self.browse(ids):
|
||||
st_file.backend_id.sudo().delete(st_file.relative_path)
|
||||
st_file.with_context(cleanning_storage_file=True).unlink()
|
||||
# commit is required since the backend could be an external system
|
||||
# therefore, if the record is deleted on the external system
|
||||
# we must be sure that the record is also deleted into Odoo
|
||||
st_file._cr.commit()
|
||||
|
||||
@api.model
|
||||
def get_from_slug_name_with_id(self, slug_name_with_id):
|
||||
"""
|
||||
Return a browse record from a string generated by the method
|
||||
_slugify_name_with_id
|
||||
:param slug_name_with_id:
|
||||
:return: a BrowseRecord (could be empty...)
|
||||
"""
|
||||
# id is the last group of digit after '-'
|
||||
_id = re.findall(r"-([0-9]+)", slug_name_with_id)[-1:]
|
||||
if _id:
|
||||
_id = int(_id[0])
|
||||
return self.browse(_id)
|
||||
Loading…
Add table
Add a link
Reference in a new issue