Skip to content

src/genro_asgi/storage_mixin.py

Source from this local checkout, regenerated when the reader rebuilds. Line links use #L<number>; a GitHub line range opens its first line.

1 # Copyright 2025 Softwell S.r.l.2 #3 # Licensed under the Apache License, Version 2.0 (the "License");4 # you may not use this file except in compliance with the License.5 # You may obtain a copy of the License at6 #7 #     https://www.apache.org/licenses/LICENSE-2.08 #9 # Unless required by applicable law or agreed to in writing, software10 # distributed under the License is distributed on an "AS IS" BASIS,11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12 # See the License for the specific language governing permissions and13 # limitations under the License.14 15 """Storage capability: genro-storage as a mixin over the base server (§4, D6).16 17 ``StorageMixin`` is a capability composed over ``BaseServer`` (``class S(...,18 StorageMixin, BaseServer)``). Unlike ``AuthMixin``/``SessionMixin`` it arms NO19 middleware — storage is survival infrastructure, not a request-chain concern20 (§4: PUBLIC = base + storage). Its cooperative ``__init__`` peels ``storage=``21 and ``storage_key=`` and forwards everything else down the D16 chain.22 23 ``storage=`` shapes the ``genro_storage.StorageManager`` the server owns:24 25 - ``None`` → a manager with the single mount ``site:``, the deployment26   directory (the process cwd);27 - a ``StorageManager`` instance → adopted as-is;28 - a ``list[dict]`` → genro-storage's own mount configuration, handed to29   ``configure()`` verbatim (``{"name": ..., "protocol": ..., ...}``); an EMPTY30   list reads like ``None`` — a recipe that declared a ``storage`` section for31   its key material alone asked for the default layout, not for no storage.32 33 ``storage_key=`` is the at-rest key material: comma-separated Fernet keys, each34 optionally ``<domain>:`` prefixed (the first key of a domain encrypts, all of35 them decrypt). It reaches ``configure(storage_key=...)``; key material that36 resolves empty is genro-storage's own boot error, never a silent37 no-encryption fallback. Omitted, encryption stays dormant and any38 ``encrypted=True`` write raises at the write site.39 40 Encryption is declared per WRITE, not per mount: the stores that hold41 credentials pass ``encrypted=True``, everything else writes plain, and both42 share one directory tree — what lands on disk is self-describing (an envelope43 whose first line is ``#GNRE1:``), so reads declare nothing.44 45 genro-storage nodes are ``smartasync``: under a running event loop they would46 hand back a coroutine instead of a value. This server's storage API is47 SYNCHRONOUS by ratification (D22, core 1b) — the stores and the spool are plain48 sync objects and async dispatch paths wrap them — so the mixin pins the sync49 dispatch (``set_sync()``) for the context the server is built in, which every50 task it later spawns inherits.51 52 A composition WITHOUT the mixin has NO ``storage`` attribute at all.53 """54 55 from __future__ import annotations56 57 from pathlib import Path58 from typing import Any59 60 from genro_storage import StorageManager61 from genro_toolbox.smartasync import set_sync62 63 __all__ = ["DEFAULT_SITE_MOUNT", "StorageMixin"]64 65 DEFAULT_SITE_MOUNT = {"name": "site", "protocol": "local"}66 """The default layout, minus its anchor: ONE local mount named ``site``.67 68 The anchor is deliberately absent — it is the cwd read when the manager is69 built, which is boot. ``BaseConfiguration.storage_mounts`` writes the same70 mount as a recipe line, so this is the one place the two agree on.71 """72 73 74 class StorageMixin:75     """Storage capability mixin, composed over the base server. Arms no middleware.76 77     Constructor kwargs peeled here: ``storage`` — ``None`` (a manager with the78     single ``site:`` mount on the deployment directory), a ``StorageManager``79     instance (adopted), or a ``list[dict]`` of genro-storage mount configs (empty80     reads like ``None``); ``storage_key`` — the at-rest key material.81     """82 83     def __init__(self, **kwargs: Any) -> None:84         storage: StorageManager | list[dict[str, Any]] | None = kwargs.pop("storage", None)85         storage_key: str | None = kwargs.pop("storage_key", None)86         set_sync()87         super().__init__(**kwargs)88         self._storage = self._build_storage(storage, storage_key)89 90     def _build_storage(91         self,92         storage: StorageManager | list[dict[str, Any]] | None,93         storage_key: str | None,94     ) -> StorageManager:95         """Turn ``storage=``/``storage_key=`` into the ``StorageManager`` the server owns."""96         if isinstance(storage, StorageManager):97             if storage_key is not None:98                 storage.set_encryption_keys(storage_key)99             return storage100         mounts = storage or self._default_mounts()101         built = StorageManager()102         built.configure(mounts, storage_key=storage_key)103         return built104 105     def _default_mounts(self) -> list[dict[str, Any]]:106         """The default configuration: one ``site:`` mount on the deployment directory."""107         return [{**DEFAULT_SITE_MOUNT, "base_path": str(Path.cwd())}]108 109     @property110     def storage(self) -> StorageManager:111         """The storage this server owns (mounts + at-rest encryption key material)."""112         return self._storage