Skip to content
infocyphPublic

About

File Management Made Simple

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

Pathwise

Security & Standards Packagist Downloads License: MIT Packagist Version Packagist PHP Version GitHub Code Size Documentation

Pathwise 4 is a framework-neutral PHP 8.4+ filesystem toolkit built on Flysystem 3. It combines safe local file operations with instance-scoped storage topology, hardened upload/download pipelines, archive controls, file-backed queueing, observability, retention, indexing, policy enforcement, and bounded trusted-local native filesystem acceleration.

Requirements

  • PHP >=8.4
  • ext-fileinfo
  • league/flysystem ^3.36
  • psr/log ^3.0.2

ZIP, POSIX ownership, XML parsing, and remote Flysystem adapters are optional capabilities. Install only the extensions/adapters your application uses.

composer require infocyph/pathwise:^4.0

Storage topology

Use StorageContext for applications, workers, long-lived runtimes, or any process that can host more than one storage topology. Contexts do not register process-global mounts.

use Infocyph\Pathwise\Storage\StorageContext;

$storage = new StorageContext([
    'primary' => ['driver' => 'local', 'root' => '/srv/app/storage'],
    'archive' => ['driver' => 'local', 'root' => '/srv/app/archive'],
], 'primary');

[$filesystem, $location] = $storage->resolve('archive://reports/q1.txt');
$filesystem->write($location, "ready\n");

$local = $storage->localPath('documents/readme.txt');

StorageFactory::createFilesystem() remains the stateless constructor for built-in/official Flysystem adapters. Custom driver factories belong to a StorageContext, not a global registry.

File and directory operations

use Infocyph\Pathwise\PathwiseFacade;

$file = PathwiseFacade::at('/tmp/example.txt')->file();
$file->create("v1\n")->append("v2\n");

$report = PathwiseFacade::at('/tmp/source')
    ->directory()
    ->syncTo('/tmp/backup', deleteOrphans: true);

The facade is stateless convenience. Persistent storage topology belongs to StorageContext.

Framework-neutral uploads

use Infocyph\Pathwise\StreamHandler\MalwareScanMode;
use Infocyph\Pathwise\StreamHandler\UploadProcessor;
use Infocyph\Pathwise\StreamHandler\UploadSource;
use Infocyph\Pathwise\StreamHandler\UploadTrustProfile;

$uploader = new UploadProcessor();
$uploader->setStorageContext($storage);
$uploader->setTrustProfile(UploadTrustProfile::UNTRUSTED_DATA);
$uploader->setDirectorySettings('primary://uploads', tempDir: sys_get_temp_dir());
$uploader->setValidationProfile('document');
$uploader->setMalwareScanMode(MalwareScanMode::REQUIRED);
$uploader->setMalwareScanner($scanner);

$source = UploadSource::fromMover(
    mover: static function (string $target) use ($uploadedFile): void {
        $uploadedFile->moveTo($target);
    },
    clientFilename: $uploadedFile->getClientFilename() ?? 'upload.bin',
    size: $uploadedFile->getSize(),
    clientMediaType: $uploadedFile->getClientMediaType(),
    error: $uploadedFile->getError(),
);

$path = $uploader->ingestSource($source);

The strict untrusted-data profile uses private bounded staging, server-generated naming, content checks, controlled publication, and restrictive local permissions. Scanner policy remains explicit: REQUIRED fails closed. The client filename is metadata only and never becomes the authoritative destination path.

Trusted public/static files

Do not map a raw URL directly to disk. Let application policy choose the public root and candidate name, then resolve that relative candidate through Pathwise:

use Infocyph\Pathwise\StreamHandler\PublicFileResolver;

$asset = (new PublicFileResolver())->resolve(
    '/srv/app/public',
    'assets/app.css',
);

// $asset->path is canonically contained and can now feed DownloadProcessor
// or an already-authorized response/file writer.

Traversal/root escape fails closed and symlink policy is explicit. Route eligibility, dotfile policy, HTTP caching/ranges, and transport remain application concerns.

Secure downloads and ranges

use Infocyph\Pathwise\StreamHandler\DownloadProcessor;

$downloads = new DownloadProcessor();
$downloads->setStorageContext($storage);
$downloads->setAllowedRoots(['primary://downloads']);

$prepared = $downloads->prepareDownload(
    'primary://downloads/video.mp4',
    rangeHeader: $_SERVER['HTTP_RANGE'] ?? null,
);

foreach ($downloads->streamChunks($prepared) as $chunk) {
    echo $chunk;
}

DownloadPreparation carries status/headers/range metadata. streamChunks() revalidates the preparation, reads exactly the prepared range, and closes the source stream even when iteration ends early.

Durable local file queue

use Infocyph\Pathwise\Queue\FileJobQueue;

$queue = new FileJobQueue('/var/lib/app/jobs.json');
$queue->enqueue('thumbnail', ['id' => 'asset-42']);

$reservation = $queue->reserve();
if ($reservation !== null) {
    // Work with $reservation->payload, renew long jobs when required.
    $queue->acknowledge($reservation);
}

The queue is intentionally direct-local: it uses typed opaque leases, stale-worker rejection, strict versioned state, locking, and crash-safe persistence. It is not a distributed broker.

For distributed deployments, let the host framework own a shared broker and use Pathwise inside workers. Separate local disks hold separate queues; network-mounted queue files are outside the supported contract. Pass shared storage keys and make handlers safe to retry. See the documentation's Queue guide for deployment and lease limits.

Optional borrowed Runwire 2.1.1 integration

Runwire is an optional host runtime, pinned to 2.1.1 for integration development and suggested for deployments using the bridge. Normal Pathwise use does not instantiate or activate Runwire. The host owns its runtime, request, scope and cancellation.

use Infocyph\Pathwise\Integration\Runwire\RunwireExecutionContext;
use Infocyph\Pathwise\StreamHandler\DownloadProcessor;

// $runtime and $request are supplied by the active host; Pathwise does not create them.
$execution = new RunwireExecutionContext($runtime, $request, $scope ?? null);
$download = new DownloadProcessor();
$result = $download->withRunwire(
    $execution,
    static fn (DownloadProcessor $bound) => $bound->prepareDownload($path),
);

The same execution object may be explicitly forwarded through intermediary services. RunwireExecutionContext::iterateChecksums() keeps the static ChecksumIndexer API unchanged and checks cancellation between completed file hashes; a single native hash remains synchronous. Bind separately inside newly created Fibers; bindings never implicitly inherit. Calls that return lazy generators must be consumed inside the scoped callback. Checkpoints validate the live request/deadline and optionally yield only inside a supplied, capable Runwire task. Blocking adapter operations, native subprocesses, arbitrary movers and filesystem calls remain synchronous; no owned cancellation source, scheduler or host lifecycle is introduced. Cancellation is checked before publication where Pathwise has a safe boundary; successfully committed publications are not retroactively reported as cancelled.

SafeFileWriter::withRunwire() makes lock retry delays cooperative when the passed scope supports coroutines. FileWatcher::watch(..., execution: $execution) uses the same borrowed context for polling intervals. Both preserve synchronous waits when the capability or context is unavailable. Waits honor the host request and scope deadlines without cancelling or closing either. ZIP extraction checkpoints each bounded 64 KiB copy and checks before publication and transaction commit; cancellation rolls back replacements and removes owned staging files.

Security model

Pathwise 4 includes explicit controls for:

  • canonical path/root containment and explicit public-root resolution;
  • private upload staging, extension/MIME/signature validation, controlled publication and optional/required malware scanning;
  • ZIP manifest validation, traversal/collision/special-entry rejection, path/name/depth bounds and extraction resource limits;
  • trusted direct-local native filesystem acceleration with shell-free argv, timeout/output ceilings and deterministic cleanup;
  • safe serialization boundaries that do not instantiate untrusted objects;
  • queue state size/payload/job limits and lease ownership;
  • policy enforcement, audit sinks, retention, indexing, and watcher workloads.

Generic application use of NativeCommandRunner is deprecated as of 4.1; application process work belongs to the host runtime. Pathwise 4.2 adds optional integration with Runwire 2.1.1 through a passed execution context. Pathwise does not own host schedulers, worker lifecycles or process managers.

Security-sensitive behavior is fail-closed where a configured capability is required. Local/remote capabilities remain explicit rather than silently emulated. Pathwise makes filesystem artifacts safe to treat as data according to policy; it does not make arbitrary uploaded/source/binary content safe to execute. See the documentation's Trust Boundaries and Persistent Runtimes guide for the full ownership model.

Security

Do not disclose suspected vulnerabilities in a public issue, discussion or pull request. Follow SECURITY.md and use GitHub private vulnerability reporting.

Pathwise is protected by PHPForge, which provides automated tests, static and taint analysis, dependency auditing, architecture checks and release-readiness gates. Automated controls do not replace responsible disclosure or manual review.


Made with ❤️ for the PHP community
MIT Licensed
Documentation • Security • Code of Conduct • Contributing
🗂️ Bug • Feature • Documentation • Question • CI failure
🔀 General • Bug fix • Feature • Refactor • Performance • Security & reliability • Documentation • Maintenance

About

File Management Made Simple

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages