northrook/php-filesystem

Filesystem operations with typed exceptions, atomic writes, and directory sync

Maintainers

Package info

codeberg.org/northrook/php-filesystem

Issues

pkg:composer/northrook/php-filesystem

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

0.2.0 2026-07-02 14:59 UTC

This package is not auto-updated.

Last update: 2026-07-03 12:18:34 UTC


README

Filesystem operations for Northrook projects.

Implements FilesystemInterface with typed exceptions, atomic writes, directory sync, and predictable semantics around paths, symlinks, and removal.

Requires PHP >=8.4 and northrook/core-contracts.

Installation

composer require northrook/php-filesystem
use Northrook\Filesystem;

$filesystem = new Filesystem();

An optional ErrorHandlerInterface may be passed to the constructor so native calls routed through box() integrate with your application error handler.

Basic usage

$filesystem->createDirectory('/var/app/cache');
$filesystem->writeFileAtomically('/var/app/config.json', '{"enabled":true}');
$filesystem->appendToFile('/var/app/log.txt', "line\n", lock: true);

$content = $filesystem->readFile('/var/app/config.json');

$filesystem->copyFile('/var/app/config.json', '/var/app/config.backup.json');
$filesystem->move('/var/app/config.backup.json', '/var/app/archive/config.json');

$filesystem->remove('/var/app/archive');

Semantics

Exceptions

Failed operations throw Northrook\Contracts\Exceptions\FilesystemException or FileNotFoundException.

Invalid use of contract-backed helpers throw Northrook\Contracts\Exceptions\RuntimeException.

Path length

Paths are validated against MAX_PATH_LENGTH of 4094 from core-contracts.

Predicates are assertive; an overlong path throws rather than returning false, unlike PHP's native file_exists()-style functions.

Atomic writes

writeFileAtomically() writes to a temporary file in the target directory, preserves existing permissions when possible, then moves the temp file into place.

When the target path is a symlink, the link is followed to its ultimate referent (cycle-safe), and the content is written there; the symlink at the original path remains.

// Safe to call repeatedly; last write wins without a torn read.
$filesystem->writeFileAtomically($path, $json);

syncDirectory

Mirrors a source directory tree into a destination.

$alwaysOverwrite

When false (default), files are copied only if the source is newer than the destination.

$deleteMissingFiles

When true, entries present in the destination but absent from the source are removed (mirror mode).

Orphan scanning uses the destination tree separately; a custom $entries iterator only drives the source walk.

Type mismatches

If a relative path exists as different types in source and destination (e.g. file vs directory), an exception is thrown unless mirror mode removes the conflicting destination entry first.

Nested destinations

Copying is skipped when the destination lies inside the source tree.

Symlinks

Symlink targets are copied verbatim unless $copyLinksOnWindows is enabled.

// Mirror deploy: copy tree and delete files no longer in source.
$filesystem->syncDirectory($buildDir, $publicDir, deleteMissingFiles: true);

move fallbacks

move() prefers a native rename(). When that fails:

  • Symlinks the link is recreated at the target; the referent is not dereferenced.
  • Directories contents are synced via relocation aside, with rollback on failure.
  • Files copy to target, then remove the source.

Parent directories for the target are created as needed.

Unless $overwrite is true, an existing file or symlink at the target is rejected.

Symlinks: copy vs move

copyFile() and move() treat symlinks at the destination differently:

  • copyFile() writes through an existing destination symlink to its referent (same inode short-circuit applies when source and referent share an inode).
  • move() replaces a symlink entry at the target, consistent with rename() semantics rather than writing through the link.

Removal and .temp!<hash>

Removing a top-level directory may first rename it aside to .temp!<hash> in the same parent directory, freeing the original path immediately while contents are deleted recursively.

A lock file (.temp!<hash>.lock) is held for the duration of that work.

Interrupted removals or cross-device moves can leave stale .temp!<hash> directories behind.

Use purgeRelocationTempDirectories() for periodic clean-up; it respects active locks, supports a minimum age filter, and can scan recursively.

// Cron-friendly: remove relocation temps older than one hour.
$removed = $filesystem->purgeRelocationTempDirectories(
    '/var/app',
    recursive: true,
    minimumAgeSeconds: 3600,
);

Windows

Creating symbolic or hard links may require elevated privileges.

When the OS reports error code 1314, the exception message includes a plain-language hint.

  • createSymlink(..., $copyDirectoryOnWindows) optionally syncs a directory instead of creating a symlink.
  • syncDirectory(..., $copyLinksOnWindows) optionally follows symlinks when iterating the source on Windows.

Ownership and recursive chmod tests are platform-dependent; see the test suite skip conditions for what applies on Windows.

Error handler injection

use Northrook\ErrorHandler;
use Northrook\Filesystem;

$errorHandler = ErrorHandler::register( $logger );
$filesystem   = new Filesystem($errorHandler);

When an ErrorHandlerInterface is provided, fallible native calls in box() are delegated to $errorHandler->box().

Without one, a lightweight internal error handler captures the last PHP error for exception messages.

Development

composer test
composer phpstan
composer test:stress   # optional; large batch / tree scenarios

License

BSD-3-Clause. See LICENSE.