jardisadapter/filesystem

Filesystem abstraction for local and cloud storage with unified read, write, and stream API

Maintainers

Package info

github.com/jardisAdapter/filesystem

Homepage

pkg:composer/jardisadapter/filesystem

Statistics

Installs: 35

Dependents: 1

Suggesters: 1

Stars: 0

Open Issues: 0

v1.0.0 2026-04-18 11:09 UTC

This package is auto-updated.

Last update: 2026-04-18 11:10:17 UTC


README

Build Status License: MIT PHP Version PHPStan Level PSR-12 Zero Dependencies

Part of the Jardis Business Platform — Enterprise-grade PHP components for Domain-Driven Design

File operations without the framework. A lean filesystem abstraction for local and S3-compatible storage — designed for DDD applications that store uploads, manage assets, or sync backups. No Flysystem, no AWS SDK, no dependency bloat. Just cURL and PHP builtins.

Why This Filesystem?

  • Two classes are enoughFilesystemService + a config object, nothing else
  • Multiple instances — local for uploads, S3 for backups, both in the same project
  • Atomic handler pipeline — each operation is its own invokable, orchestrated by closures
  • Stream support — read and write large files without memory overhead
  • S3 without the SDK — AWS Signature v4 via cURL, works with MinIO, DigitalOcean Spaces, etc.
  • Security hardened — path traversal protection, symlink containment, XXE prevention, secret masking
  • 79% test coverage — integration tests against real MinIO, not mocks

Installation

composer require jardisadapter/filesystem

Quick Start

Local Filesystem

use JardisAdapter\Filesystem\FilesystemService;

$service = new FilesystemService();
$fs = $service->local('/var/app/storage');

$fs->write('uploads/photo.jpg', $imageData);
$content = $fs->read('uploads/photo.jpg');

S3-Compatible Storage

$fs = $service->s3(
    bucket: 'my-bucket',
    region: 'eu-central-1',
    key: 'AKIAEXAMPLE',
    secret: 'wJalrXUtnFEMI/K7MDENG...',
);

$fs->write('backups/dump.sql', $sqlDump);

Multiple Backends

$uploads = $service->local('/storage/uploads');
$backups = $service->s3('company-backups', 'eu-central-1', $env('AWS_KEY'), $env('AWS_SECRET'));

// Upload lokal speichern
$uploads->write('invoice-2026.pdf', $pdf);

// Backup auf S3 sichern
$backups->write('daily/invoice-2026.pdf', $pdf);

Advanced Configuration

For custom permissions, symlink settings, or other advanced options — use create() with a config object:

use JardisAdapter\Filesystem\Config\LocalConfig;

$fs = $service->create(new LocalConfig(
    root: '/storage/uploads',
    filePermissions: 0600,
    dirPermissions: 0700,
    followSymlinks: false,
));

File Operations

$fs->write('file.txt', 'content');
$fs->read('file.txt');                  // string
$fs->exists('file.txt');                // bool
$fs->delete('file.txt');
$fs->copy('source.txt', 'target.txt');
$fs->move('old.txt', 'new.txt');
$fs->size('file.txt');                  // int (bytes)
$fs->lastModified('file.txt');          // int (unix timestamp)
$fs->mimeType('file.txt');              // string

Stream Support

For large files — no memory overhead:

// Write from stream
$stream = fopen('/tmp/video.mp4', 'rb');
$fs->writeStream('videos/intro.mp4', $stream);
fclose($stream);

// Read as stream
$stream = $fs->readStream('videos/intro.mp4');
while (!feof($stream)) {
    $chunk = fread($stream, 8192);
    // process chunk...
}
fclose($stream);

Directory Operations

$fs->createDirectory('uploads/2026');
$fs->deleteDirectory('uploads/2025');    // recursive

foreach ($fs->listContents('uploads', recursive: true) as $item) {
    echo $item->path();           // 'uploads/photo.jpg'
    echo $item->size();           // 1048576
    echo $item->lastModified();   // 1711929600
    echo $item->isFile();         // true
    echo $item->isDirectory();    // false
}

Visibility

Control file permissions (local: Unix chmod, S3: ACL):

$fs->setVisibility('public/logo.png', 'public');
$fs->setVisibility('private/secret.pdf', 'private');

$fs->getVisibility('public/logo.png');   // 'public'

Configuration

LocalConfig

new LocalConfig(
    root: '/var/app/storage',       // required — validated via realpath()
    filePermissions: 0644,          // new files (default: 0644)
    dirPermissions: 0755,           // new directories (default: 0755)
    followSymlinks: true,           // follow symlinks (default: true)
    publicFilePerms: 0644,          // visibility 'public' files
    privateFilePerms: 0600,         // visibility 'private' files
    publicDirPerms: 0755,           // visibility 'public' directories
    privateDirPerms: 0700,          // visibility 'private' directories
)

S3Config

new S3Config(
    bucket: 'my-bucket',                        // required
    region: 'eu-central-1',                      // required
    key: 'AKIAEXAMPLE',                          // required
    secret: 'wJalrXUtnFEMI...',                  // required, masked in debug output
    endpoint: 'https://s3.amazonaws.com',        // default: AWS (use custom for MinIO etc.)
    prefix: 'uploads/',                          // path prefix in bucket (default: '')
)

The secret is protected with #[\SensitiveParameter] and masked in var_dump() / debug output.

Error Handling

All exceptions implement FilesystemExceptionInterface — catch one, catch all:

Exception When
FileNotFoundException File or directory does not exist
UnableToReadException Read failure (permissions, I/O, S3 auth)
UnableToWriteException Write failure (permissions, disk full, S3)
UnableToDeleteException Delete failure
FilesystemException Base — path traversal, null byte, invalid config
use JardisAdapter\Filesystem\Exception\FileNotFoundException;
use JardisSupport\Contract\Filesystem\FilesystemExceptionInterface;

try {
    $content = $fs->read('missing.txt');
} catch (FileNotFoundException $e) {
    // file does not exist
} catch (FilesystemExceptionInterface $e) {
    // any other filesystem error
}

Architecture

The user only sees FilesystemService + config objects. Internally, the orchestrator builds a pipeline of atomic invokable handlers — one __invoke per operation:

FilesystemService (implements FilesystemServiceInterface)
  ├── local(root): FilesystemInterface
  ├── s3(bucket, region, key, secret): FilesystemInterface
  └── create(LocalConfig|S3Config): FilesystemInterface   ← advanced

Filesystem (Orchestrator)
  │
  │  PathNormalizer — traversal + null byte protection
  │
  ├── Local:
  │   LocalFullPath (symlink containment via realpath)
  │   + 16 atomic handlers: LocalRead, LocalWrite, LocalExists, ...
  │
  └── S3:
      S3Signer (AWS Signature v4)
      S3Request (shared cURL helper)
      + 16 atomic handlers: S3Read, S3Write, S3Exists, ...

Each handler is an invokable object (__invoke) — independently testable, replaceable, composable. The orchestrator extracts closures via ->__invoke(...) and stores only the closures. No handler object survives as a property.

Security

  • Path traversal.. segments and null bytes rejected before any I/O
  • Symlink containmentrealpath() check ensures resolved paths stay inside root
  • Root validationLocalConfig resolves root via realpath() at construction time
  • XXE preventionLIBXML_NONET on all XML parsing (S3 responses)
  • Secret maskingS3Config::$secret uses #[\SensitiveParameter] + __debugInfo()
  • Bucket wipe guarddeleteDirectory('') with empty prefix is rejected

Contracts

The package implements interfaces from jardissupport/contract:

Interface Purpose
FilesystemServiceInterface Factory: local(), s3()
FilesystemInterface Full API (extends Reader + Writer)
FilesystemReaderInterface Read-only subset — inject this for read-only contexts
FilesystemWriterInterface Write-only subset
// Inject read-only access
public function __construct(
    private readonly FilesystemReaderInterface $storage,
) {}

Jardis Foundation Integration

In a Jardis DDD project, the filesystem is available via the resource chain:

$uploads = $this->getResource()->filesystem()->local('/storage/uploads');

$backups = $this->getResource()->filesystem()->s3(
    bucket: $env('FS_BACKUPS_BUCKET'),
    region: $env('FS_BACKUPS_REGION'),
    key: $env('FS_BACKUPS_KEY'),
    secret: $env('FS_BACKUPS_SECRET'),
);

No singleton, no handler — the developer decides how many filesystem instances exist and how they are configured. The resource chain returns FilesystemServiceInterface.

Development

cp .env.example .env    # One-time setup
make install             # Install dependencies
make start               # Start MinIO (S3 integration tests)
make phpunit             # Run tests (118 tests, starts MinIO automatically)
make phpstan             # Static analysis (Level 8)
make phpcs               # Coding standards (PSR-12)

Documentation

Full documentation, guides, and API reference:

docs.jardis.io/en/adapter/filesystem

License

MIT License — free for any use, including commercial.

KI-gestützte Entwicklung

Dieses Package liefert einen Skill für Claude Code, Cursor, Continue und Aider mit. Installation im Konsumentenprojekt:

composer require --dev jardis/dev-skills

Mehr Details: https://docs.jardis.io/skills