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