Source code for openfactory.filelayer.nfs_backend
"""
NFS (POSIX filesystem) implementation of the FileBackend interface for OpenFactory.
This backend allows OpenFactory services to interact with a local or
network-mounted filesystem (NFS share) using the standard FileBackend API.
Note:
- This backend is compatible with Docker Swarm and can generate
mount specifications via :meth:`.NFSBackend.get_mount_spec`.
- Ensure the NFS server is reachable and that mount options are valid
for your environment.
.. seealso::
The schema of the NFS Backend is :class:`openfactory.schemas.filelayer.nfs_backend.NFSBackendConfig`.
"""
import hashlib
from pathlib import Path
from typing import IO, List, Dict, Any
from openfactory.filelayer.backend import FileBackend
from openfactory.schemas.filelayer.nfs_backend import NFSBackendConfig
[docs]
class NFSBackend(FileBackend):
"""
FileBackend implementation for POSIX/NFS filesystems.
"""
[docs]
def __init__(self, config: NFSBackendConfig):
"""
Initialize the NFSBackend.
Args:
config (NFSBackendConfig): Configuration object for the NFS backend.
"""
self.config = config
self.root = Path(config.mount_point)
def _full_path(self, path: str) -> Path:
"""
Compute full path relative to backend root.
Args:
path (str): Relative or absolute path.
Returns:
Path: Absolute Path object.
Raises:
ValueError: If an absolute path is outside the backend root.
"""
p = Path(path)
if p.is_absolute() and not p.is_relative_to(self.root):
raise ValueError(f"Path {p} is outside backend root {self.root}")
if p.is_absolute():
# Make absolute path relative to root
p = p.relative_to(self.root)
return self.root / p
[docs]
def open(self, path: str, mode: str = "r") -> IO:
"""
Open a file at the given path.
Args:
path (str): Path to the file relative to the backend root.
mode (str): File mode ('r', 'w', 'rb', 'wb', etc.). Defaults to 'r'.
Returns:
IO: File-like object supporting read/write operations.
Raises:
FileNotFoundError: If the file does not exist when opening in read mode.
"""
full_path = self._full_path(path)
# Ensure parent directories exist when writing
if "w" in mode or "a" in mode or "x" in mode:
full_path.parent.mkdir(parents=True, exist_ok=True)
return full_path.open(mode)
[docs]
def exists(self, path: str) -> bool:
"""
Check whether a file exists at the given path.
Args:
path (str): Path to the file relative to the backend root.
Returns:
bool: True if the file exists, False otherwise.
"""
return self._full_path(path).exists()
[docs]
def delete(self, path: str) -> None:
"""
Delete the file at the given path.
Args:
path (str): Path to the file relative to the backend root.
Raises:
FileNotFoundError: If the file does not exist.
"""
full_path = self._full_path(path)
if not full_path.exists():
raise FileNotFoundError(f"File {full_path} does not exist.")
full_path.unlink()
[docs]
def listdir(self, path: str) -> List[str]:
"""
List all files and directories at the given path.
Args:
path (str): Path to the directory relative to the backend root.
Returns:
List[str]: List of file and directory names.
Raises:
FileNotFoundError: If the directory does not exist.
"""
full_path = self._full_path(path)
if not full_path.exists() or not full_path.is_dir():
raise FileNotFoundError(f"Directory {full_path} does not exist.")
return [f.name for f in full_path.iterdir()]
[docs]
@staticmethod
def from_config(config: dict) -> "NFSBackend":
"""
Create an NFSBackend instance from configuration.
Args:
config (dict): Configuration dictionary.
Returns:
NFSBackend: Configured backend instance.
Raises:
pydantic.ValidationError: If the configuration is invalid.
"""
validated_config = NFSBackendConfig(**config)
return NFSBackend(validated_config)
[docs]
def make_volume_name(self) -> str:
"""
Generate a deterministic NFS volume name for Docker.
The volume name is constructed from the NFS server address and the
remote path. If mount options are provided, a short SHA1 hash of the
normalized (sorted) options is appended to ensure uniqueness between
different configurations (e.g., `ro` vs `rw`).
Format:
``nfs_<server>_<remote_path>[_<hash>]``
Returns:
str: A deterministic volume name safe to use in Docker Swarm.
"""
base_name = (
f"nfs_{self.config.server.replace('.', '_')}_"
f"{self.config.remote_path.strip('/').replace('/', '_')}"
)
if self.config.mount_options:
# Normalize + sort options so order does not matter
opts_str = ",".join(sorted(self.config.mount_options))
digest = hashlib.sha1(opts_str.encode()).hexdigest()[:8]
return f"{base_name}_{digest}"
return base_name
[docs]
def get_mount_spec(self) -> Dict[str, Any]:
"""
Build a Docker-compatible mount specification for NFS.
This specification can be used directly in
Docker Swarm service creation or container deployment.
Returns:
Dict[str, Any]: A dictionary containing the mount configuration in Docker's expected format.
"""
# Build NFS mount options
mount_opts = [f"addr={self.config.server}"]
if self.config.mount_options:
mount_opts.extend(self.config.mount_options)
opts = {
"type": "nfs",
"o": ",".join(mount_opts),
"device": f":{self.config.remote_path}",
}
return {
"Type": "volume",
"Source": self.make_volume_name(),
"Target": self.config.mount_point,
"VolumeOptions": {
"DriverConfig": {
"Name": "local",
"Options": opts,
}
},
}