FileSystem¶
Purpose¶
FileSystem is a protocol defining the storage-access contract used by ETLRelay's file sources and sinks.
LocalFileSystem implements this protocol by providing readable and writable binary streams for local filesystem paths.
Storage access is separated from file-format handling. LocalFileSystem does not know about CSV, Parquet, Batch, readers, writers, sources, sinks, or pipeline orchestration.
For local filesystem security, LocalFileSystem uses PathValidator to ensure that paths resolve within the configured permitted directory.
Class Diagram¶
classDiagram
class FileSystem {
<<protocol>>
+open_read(path: str) BinaryIO
+open_write(path: str) BinaryIO
}
class LocalFileSystem {
-_path_validator: PathValidator
+open_read(path: str) BinaryIO
+open_write(path: str) BinaryIO
}
class PathValidator {
-base_path: Path
+validate(path: Path | str) Path
}
class FileSource {
-path: str
-storage: FileSystem
-reader: Reader
+read() Iterable~Batch~
}
class FileSink {
-path: str
-storage: FileSystem
-writer: Writer
+write(batches: Iterable~Batch~) None
}
class Reader {
<<protocol>>
+read(stream: BinaryIO) Iterable~Batch~
}
class Writer {
<<protocol>>
+write(batches: Iterable~Batch~, stream: BinaryIO) None
}
FileSystem <|.. LocalFileSystem
LocalFileSystem --> PathValidator : validates paths with
FileSource --> FileSystem : uses
FileSink --> FileSystem : uses
FileSource --> Reader : uses
FileSink --> Writer : uses
Responsibilities¶
FileSystem¶
FileSystem defines the minimal storage contract required by file sources and sinks.
It is responsible for exposing operations that open resources as binary streams:
open_read()opens a resource for reading.open_write()opens a resource for writing.
FileSystem is not responsible for:
- Parsing or serializing file formats.
- Creating or interpreting
Batchobjects. - Implementing source or sink orchestration.
- Pipeline execution.
- Application-level logging.
The protocol is intentionally small so that multiple storage implementations can satisfy the same contract.
LocalFileSystem¶
LocalFileSystem is responsible for:
- Opening local files for binary reading.
- Opening local files for binary writing.
- Validating path containment through
PathValidator. - Checking whether an input path exists before opening it.
- Returning binary streams to callers.
- Propagating filesystem access errors.
LocalFileSystem is not responsible for:
- Parsing CSV, Parquet, or other formats.
- Serializing
Batchobjects. - Selecting readers or writers.
- Creating missing parent directories.
- Source or sink orchestration.
- Pipeline execution.
- Application-level logging.
The storage implementation handles filesystem existence and I/O behavior. PathValidator handles path containment.
PathValidator¶
PathValidator determines whether a candidate path resolves within its configured base directory.
It accepts paths represented as Path objects or strings and returns the resolved path when the containment check succeeds.
It does not check whether the path exists.
This separation supports both input and output paths:
FileSource
│
└── Existing input path
│
▼
LocalFileSystem
│
▼
open_read()
FileSink
│
└── Existing or new output path
│
▼
LocalFileSystem
│
▼
open_write()
Dependency Relationship¶
FileSource and FileSink depend on the FileSystem protocol rather than directly on LocalFileSystem.
FileSource ──┐
├──> FileSystem
FileSink ────┘
▲
│ implements
│
LocalFileSystem
│
▼
PathValidator
The local implementation is responsible for local filesystem behavior. A future remote storage implementation would provide its own resource-access and security behavior while satisfying the same FileSystem contract.
Potential future implementations include:
FileSystem
├── LocalFileSystem
├── S3Storage
└── HdfsStorage
These are extension possibilities, not implementations currently provided by this component.
Data Flow¶
FileSource¶
FileSource
│
├── FileSystem.open_read(path)
│ │
│ ▼
│ BinaryIO
│ │
│ ▼
└────── Reader.read(stream)
│
▼
Iterable[Batch]
The storage component opens the resource, and the reader interprets its contents. The reader may produce multiple batches from one resource.
FileSink¶
FileSink
│
├── FileSystem.open_write(path)
│ │
│ ▼
│ BinaryIO
│ │
│ ▼
└────── Writer.write(batches, stream)
│
▼
Serialized data
The writer serializes the supplied batches into the output stream. The storage component determines where the stream is stored.
The file source or sink is responsible for managing the stream's lifetime. Streams should be closed after use, including when reading or writing raises an exception.
Interface¶
FileSystem¶
from typing import BinaryIO, Protocol
class FileSystem(Protocol):
def open_read(self, path: str) -> BinaryIO:
...
def open_write(self, path: str) -> BinaryIO:
...
LocalFileSystem¶
from typing import BinaryIO
from etlrelay.security.path import PathValidator
class LocalFileSystem:
def __init__(self, path_validator: PathValidator) -> None:
self._path_validator = path_validator
def open_read(self, path: str) -> BinaryIO:
...
def open_write(self, path: str) -> BinaryIO:
...
Contract¶
| Operation | Expected behavior |
|---|---|
open_read(path) |
Validates path containment, checks existence, and opens a readable binary stream |
open_write(path) |
Validates path containment and opens a writable binary stream |
| Read a nonexistent path | Raises FileSystemError |
| Write to a new destination with an existing parent | Creates the destination file |
| Write to an existing destination | Opens the file in binary write mode and truncates its existing contents |
| Write when the parent directory is missing | Propagates the filesystem error |
| Access a path outside the permitted base | Raises PathValidationError |
| Encounter another filesystem I/O failure | Propagates the underlying filesystem error |
LocalFileSystem.open_write() does not automatically create parent directories.
The protocol does not prescribe how all storage implementations handle resource existence, permissions, or provider-specific failures. Those behaviors belong to the concrete storage implementation.
SOLID Considerations¶
Single Responsibility Principle¶
LocalFileSystem has one primary responsibility: providing local filesystem access.
Path containment is delegated to PathValidator, while parsing and serialization are delegated to readers and writers.
Open/Closed Principle¶
Additional storage implementations can satisfy FileSystem without requiring modifications to FileSource or FileSink.
The current implementation does not require a storage factory, registry, or plugin mechanism.
Liskov Substitution Principle¶
Any implementation satisfying FileSystem can be supplied to a file source or sink, provided it preserves the protocol's behavioral contract.
Implementations must return usable binary streams and document their storage-specific behavior consistently.
Interface Segregation Principle¶
FileSystem exposes only two operations:
- Opening a resource for reading.
- Opening a resource for writing.
It does not expose unrelated filesystem administration operations such as directory traversal, permissions management, metadata management, or deletion.
Dependency Inversion Principle¶
FileSource and FileSink depend on FileSystem, not on LocalFileSystem.
LocalFileSystem depends on PathValidator to enforce local path containment.
This keeps the higher-level components independent of local filesystem details.
Design Constraints¶
The storage protocol operates on binary streams rather than requiring callers to read or write an entire resource as bytes.
LocalFileSystem
│
│ BinaryIO
▼
file resource
This allows readers and writers to operate on streams without requiring the storage component to load the entire serialized resource into memory first.
The same format implementation can therefore be composed with different storage backends:
LocalFileSystem ──┐
S3Storage ────────┼──> BinaryIO ──> Reader
HdfsStorage ──────┘
And in the reverse direction:
Writer ──> BinaryIO ──> LocalFileSystem
├─> S3Storage
└─> HdfsStorage
LocalFileSystem should continue using Python's standard filesystem APIs and pathlib.Path internally.
PathValidator remains responsible for local path containment. LocalFileSystem remains responsible for existence checks and filesystem operations.
No additional abstractions such as storage factories, registries, remote filesystem base classes, or generic resource managers are required at this stage.