# Copyright 2025 Softwell S.r.l.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Storage capability: genro-storage as a mixin over the base server (§4, D6).
``StorageMixin`` is a capability composed over ``BaseServer`` (``class S(...,
StorageMixin, BaseServer)``). Unlike ``AuthMixin``/``SessionMixin`` it arms NO
middleware — storage is survival infrastructure, not a request-chain concern
(§4: PUBLIC = base + storage). Its cooperative ``__init__`` peels ``storage=``
and ``storage_key=`` and forwards everything else down the D16 chain.
``storage=`` shapes the ``genro_storage.StorageManager`` the server owns:
- ``None`` → a manager with the single mount ``site:``, the deployment
directory (the process cwd);
- a ``StorageManager`` instance → adopted as-is;
- a ``list[dict]`` → genro-storage's own mount configuration, handed to
``configure()`` verbatim (``{"name": ..., "protocol": ..., ...}``); an EMPTY
list reads like ``None`` — a recipe that declared a ``storage`` section for
its key material alone asked for the default layout, not for no storage.
``storage_key=`` is the at-rest key material: comma-separated Fernet keys, each
optionally ``<domain>:`` prefixed (the first key of a domain encrypts, all of
them decrypt). It reaches ``configure(storage_key=...)``; key material that
resolves empty is genro-storage's own boot error, never a silent
no-encryption fallback. Omitted, encryption stays dormant and any
``encrypted=True`` write raises at the write site.
Encryption is declared per WRITE, not per mount: the stores that hold
credentials pass ``encrypted=True``, everything else writes plain, and both
share one directory tree — what lands on disk is self-describing (an envelope
whose first line is ``#GNRE1:``), so reads declare nothing.
genro-storage nodes are ``smartasync``: under a running event loop they would
hand back a coroutine instead of a value. This server's storage API is
SYNCHRONOUS by ratification (D22, core 1b) — the stores and the spool are plain
sync objects and async dispatch paths wrap them — so the mixin pins the sync
dispatch (``set_sync()``) for the context the server is built in, which every
task it later spawns inherits.
A composition WITHOUT the mixin has NO ``storage`` attribute at all.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
from genro_storage import StorageManager
from genro_toolbox.smartasync import set_sync
__all__ = ["DEFAULT_SITE_MOUNT", "StorageMixin"]
DEFAULT_SITE_MOUNT = {"name": "site", "protocol": "local"}
"""The default layout, minus its anchor: ONE local mount named ``site``.
The anchor is deliberately absent — it is the cwd read when the manager is
built, which is boot. ``BaseConfiguration.storage_mounts`` writes the same
mount as a recipe line, so this is the one place the two agree on.
"""
[docs]
class StorageMixin:
"""Storage capability mixin, composed over the base server. Arms no middleware.
Constructor kwargs peeled here: ``storage`` — ``None`` (a manager with the
single ``site:`` mount on the deployment directory), a ``StorageManager``
instance (adopted), or a ``list[dict]`` of genro-storage mount configs (empty
reads like ``None``); ``storage_key`` — the at-rest key material.
"""
def __init__(self, **kwargs: Any) -> None:
storage: StorageManager | list[dict[str, Any]] | None = kwargs.pop("storage", None)
storage_key: str | None = kwargs.pop("storage_key", None)
set_sync()
super().__init__(**kwargs)
self._storage = self._build_storage(storage, storage_key)
def _build_storage(
self,
storage: StorageManager | list[dict[str, Any]] | None,
storage_key: str | None,
) -> StorageManager:
"""Turn ``storage=``/``storage_key=`` into the ``StorageManager`` the server owns."""
if isinstance(storage, StorageManager):
if storage_key is not None:
storage.set_encryption_keys(storage_key)
return storage
mounts = storage or self._default_mounts()
built = StorageManager()
built.configure(mounts, storage_key=storage_key)
return built
def _default_mounts(self) -> list[dict[str, Any]]:
"""The default configuration: one ``site:`` mount on the deployment directory."""
return [{**DEFAULT_SITE_MOUNT, "base_path": str(Path.cwd())}]
@property
def storage(self) -> StorageManager:
"""The storage this server owns (mounts + at-rest encryption key material)."""
return self._storage