diff --git a/src/viur/core/bones/base.py b/src/viur/core/bones/base.py index 502329fa8..17a42b292 100644 --- a/src/viur/core/bones/base.py +++ b/src/viur/core/bones/base.py @@ -985,14 +985,12 @@ def unserialize_compute(self, skel: "SkeletonInstance", name: str) -> bool: case ComputeMethod.Lifetime: now = utils.utcNow() from viur.core.skeleton import RefSkel # noqa: E402 # import works only here because circular imports - if issubclass(skel.skeletonCls, RefSkel): # we have a ref skel we must load the complete Entity db_obj = db.Get(skel["key"]) last_update = db_obj.get(f"_viur_compute_{name}_") else: last_update = skel.dbEntity.get(f"_viur_compute_{name}_") skel.accessedValues[f"_viur_compute_{name}_"] = last_update or now - if not last_update or last_update + self.compute.interval.lifetime <= now: # if so, recompute and refresh updated value skel.accessedValues[name] = value = self._compute(skel, name) diff --git a/src/viur/core/bones/file.py b/src/viur/core/bones/file.py index a4a0fdf70..c7a542d01 100644 --- a/src/viur/core/bones/file.py +++ b/src/viur/core/bones/file.py @@ -146,6 +146,8 @@ def __init__( "height", "derived", "public", + "download_url_json", + "download_url_html" ), public: bool = False, **kwargs diff --git a/src/viur/core/config.py b/src/viur/core/config.py index 9a283a10d..13b1840b2 100644 --- a/src/viur/core/config.py +++ b/src/viur/core/config.py @@ -804,6 +804,24 @@ class Conf(ConfigType): file_thumbnailer_url: t.Optional[str] = None # TODO: """docstring""" + file_generate_download_url_for_derives: bool | t.Iterable[str] = False + """ + If True, for all derives, a download URL is created as well. + + When an iterable of str is provided, for all entries that match a pattern, + a download URL will be created. + + Example: + ```py + class Test(Skeleton): + image_bar = FileBone(derive=conf["derives"]) + image_foo = FileBone(derive=conf["derives"]) + logo = FileBone(derive=conf["derives"]) + + conf.file_generate_download_url_for_derives = ["test.image*"] + ``` + """ + main_app: "Module" = None """Reference to our pre-build Application-Instance""" diff --git a/src/viur/core/modules/file.py b/src/viur/core/modules/file.py index 063310253..bde0aa498 100644 --- a/src/viur/core/modules/file.py +++ b/src/viur/core/modules/file.py @@ -22,7 +22,8 @@ from google.cloud import storage from google.oauth2.service_account import Credentials as ServiceAccountCredentials from viur.core import conf, current, db, errors, utils -from viur.core.bones import BaseBone, BooleanBone, KeyBone, NumericBone, StringBone +from viur.core.bones import BaseBone, BooleanBone, KeyBone, NumericBone, StringBone, UriBone +from viur.core.bones import Compute, ComputeMethod, ComputeInterval from viur.core.decorators import * from viur.core.i18n import LanguageWrapper from viur.core.prototypes.tree import SkelType, Tree, TreeSkel @@ -281,20 +282,6 @@ def make_request(): return reslist -class DownloadUrlBone(BaseBone): - """ - This bone is used to inject a freshly signed download url into a FileSkel. - """ - - def unserialize(self, skel, name): - if "dlkey" in skel.dbEntity and "name" in skel.dbEntity: - skel.accessedValues[name] = File.create_download_url( - skel["dlkey"], skel["name"], expires=conf.render_json_download_url_expiration - ) - return True - - return False - class FileLeafSkel(TreeSkel): """ @@ -350,10 +337,37 @@ class FileLeafSkel(TreeSkel): searchable=True, ) - downloadUrl = DownloadUrlBone( - descr="Download-URL", + download_url_json = UriBone( + descr="Download-URL for HTML", readOnly=True, visible=False, + compute=Compute( + fn=lambda skel: File.create_download_url( + dlkey=skel["dlkey"], + filename=skel["name"], + expires=conf.render_json_download_url_expiration + ), + interval=ComputeInterval( + method=ComputeMethod.Lifetime, + lifetime=conf.render_json_download_url_expiration or datetime.timedelta(hours=1) + ) + ) + ) + download_url_html = UriBone( + descr="Download-URL for HTML", + readOnly=True, + visible=False, + compute=Compute( + fn=lambda skel: File.create_download_url( + dlkey=skel["dlkey"], + filename=skel["name"], + expires=conf.render_html_download_url_expiration + ), + interval=ComputeInterval( + method=ComputeMethod.Lifetime, + lifetime=conf.render_html_download_url_expiration or datetime.timedelta(hours=1) + ) + ) ) derived = BaseBone( diff --git a/src/viur/core/render/json/default.py b/src/viur/core/render/json/default.py index bbd8e5b0e..5d69461d9 100644 --- a/src/viur/core/render/json/default.py +++ b/src/viur/core/render/json/default.py @@ -1,7 +1,8 @@ +import fnmatch import json import typing as t from enum import Enum - +from viur.core.modules.file import File from viur.core import bones, db, current from viur.core.render.abstract import AbstractRenderer from viur.core.skeleton import SkeletonInstance @@ -88,7 +89,11 @@ def renderSingleBoneValue(self, value: t.Any, if isinstance(bone, bones.RelationalBone): if isinstance(value, dict): return { - "dest": self.renderSkelValues(value["dest"], injectDownloadURL=isinstance(bone, bones.FileBone)), + "dest": self.renderSkelValues( + value["dest"], + bone_path=f"{skel.kindName}.{bone.name}", + injectDownloadURL=isinstance(bone, bones.FileBone) + ), "rel": (self.renderSkelValues(value["rel"], injectDownloadURL=isinstance(bone, bones.FileBone)) if value["rel"] else None), } @@ -122,7 +127,10 @@ def renderBoneValue(self, bone: bones.BaseBone, skel: SkeletonInstance, key: str res = self.renderSingleBoneValue(boneVal, bone, skel, key) return res - def renderSkelValues(self, skel: SkeletonInstance, injectDownloadURL: bool = False) -> t.Optional[dict]: + def renderSkelValues(self, + skel: SkeletonInstance, + bone_path: t.Optional[str] = None, + injectDownloadURL: bool = False) -> t.Optional[dict]: """ Prepares values of one :class:`viur.core.skeleton.Skeleton` or a list of skeletons for output. @@ -140,15 +148,38 @@ def renderSkelValues(self, skel: SkeletonInstance, injectDownloadURL: bool = Fal if ( injectDownloadURL - and (file := getattr(conf.main_app, "file", None)) and "dlkey" in skel and "name" in skel ): - res["downloadUrl"] = file.create_download_url( + res["downloadUrl"] = File.create_download_url( skel["dlkey"], skel["name"], expires=conf.render_json_download_url_expiration ) + + # generate the downloadUrl for derives + search_paths = [] + if search_path := current.request.get().request.headers.get("X-VIUR-DERIVED-DOWNLOAD-URL"): + search_paths.extend(search_path.split(",")) + if conf.file_generate_download_url_for_derives: + + if isinstance(conf.file_generate_download_url_for_derives, t.Iterable): + search_paths.extend(conf.file_generate_download_url_for_derives) + else: + search_paths.append("*") + + for search_path in search_paths: + if fnmatch.fnmatch(bone_path, search_path): + break + else: + return res + + for derive_name in res.get("derived", {}).get("files", {}): + res["derived"]["files"][derive_name]["downloadUrl"] = File.create_download_url( + skel["dlkey"], + derive_name, + derived=True + ) return res def renderEntry(self, skel: SkeletonInstance, actionName, params=None):