Skip to content

A simple and modern filesystem abstraction layer for PHP

Notifications You must be signed in to change notification settings

Radiergummi/filesystem

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FileSystem

A convenient abstraction layer for file system operations, wrapping all kinds of storage types with a single API.

Features

  • Use any kind of storage backend interchangeably. FileSystem includes a generic AdapterInterface for storage adapters. They can be used as drop-in replacements.
  • Streams and generators. FileSystem relies on PSR-7 and PSR 17 streams for reading file content. This has massive benefits in terms of performance, as adapters can stream file content on demand.
    Directory content is delivered as traversable generators, reducing the memory footprint even further.
  • Opt-in Caching. FileSystem supports PSR-16 caches to improve performance, independent of the adapter used.
  • Common Exceptions for common errors. FileSystem provides a range of default exceptions for common error cases that might occur in any implementation: Missing or existing files, bad permissions or invalid target path specs, for example.
  • Easily extensible. FileSystem exposes several interfaces, making it really easy to add new adapters.

Why shouldn't I just use FlySystem?

While FlySystem is an excellent library and has always been my go-to solution whenever I had to work with files, I think it has outlived it's usefulness: Since it's conception, the PHP ecosystem has vastly improved. We now have PSR standards, ubiquitous exception handling, type hints and generators. FlySystem does not make use of any of these and in some cases the maintainers have declared outright resistance.

FileSystem strives to provide a modern, fast and standards-compliant way to work with storage, independent of the underlying implementation.

Installation

Note: FileSystem is still in early development and there is no package available via composer yet. If you would like to contribute, fork the project and clone it via git.

Install using composer:

composer require radiergummi/filesystem

Usage

In the simplest case, all you need is an adapter and a FileSystem instance:

use Radiergummi\FileSystem\Adapters\LocalAdapter;
use Radiergummi\FileSystem\Exceptions\FileSystemException;
use Radiergummi\FileSystem\FileSystem;

$adapter = new LocalAdapter();
$fileSystem = new FileSystem($adapter);

try {
    $file = $fileSystem->readFile('/foo/bar.txt');
    $contents = $file->getStream(); // PSR-7 StreamInterface
    $metaData = $file->getMetaData(); 
} catch (FileSystemException $exception) {
    // Handle the error
    $exception->getPath(); // Getter for the affected path
}

Method reference

The file system exposes the following methods:

Exists

Checks whether an entity exists at a given path. This works for both files and directories.

Signature:

public function exists(string $path): bool

Possible errors:

  • Entity Not Found. Will be thrown if the entity does not exist.
  • Entity Is Not Accessible. Will be thrown if the entity is not accessible due to insufficient permissions, or other OS-level errors.
  • Root Violation. Will be thrown if the entity is located outside of the configured file system root.

Read File

Reads a file at a given path and returns a new File instance. Files expose a range of convenience methods in addition to getters for the content stream and MetaData instance.

Signature:

public function readFile(string $path): ?File

Possible errors:

Get Meta Data

Retrieves a MetaData instance, if the adapter supports it. Null will be returned otherwise.

Signature:

public function getMetaData(string $path): ?MetaData

Possible errors:

  • Entity Not Found. Will be thrown if the file or directory does not exist.
  • Entity Is Not Accessible. Will be thrown if the entity is not accessible due to insufficient permissions, or other OS-level errors.
  • Root Violation. Will be thrown if the entity is located outside of the configured file system root.

Read Directory

Lists the contents of a directory at the given path. The method always returns a generator, allowing you to efficiently iterate the results, even if they grow very large. It's up to the adapter implementation to make use of the generator, allowing for both files being fetched during iteration or beforehand, including pagination handling etc.
The results will be instances of FileSystemEntity, giving you access to their content and meta data.

Adapters that don't support directories should handle this call by returning a flat list of files or use the path to otherwise figure out the files the user is looking for.

Signature:

public function readDirectory(?string $path = null, bool $recursive = false): Generator<FileSystemEntity>

Possible errors:

  • Entity Not Found. Will be thrown if the directory does not exist.
  • Entity Is Not A Directory. Will be thrown if the entity at the target path is not a directory.
  • Entity Is Not Accessible. Will be thrown if the directory is not accessible due to insufficient permissions, or other OS-level errors.
  • Root Violation. Will be thrown if the directory is located outside of the configured file system root.

Write File

Writes to a file at the given path.

Signature:

public function writeFile(string $path, StreamInterface $contents, ?int $flags = null): void

Possible errors:

  • Entity Is Not A Directory. Will be thrown if the parent segment of the file path is not a directory.
  • Entity Is Not Accessible. Will be thrown if the file is not accessible due to insufficient permissions, or other OS-level errors.
  • Root Violation. Will be thrown if the file is located outside of the configured file system root.

Rename

Renames a file or directory in place: While the first parameter has to be the full file path, the second is designed to be the new file base name only. Therefore, to rename /foo/bar/baz.txt to /foo/bar/quz.json, you would call it as rename('/foo/bar/baz.txt', 'quz.json'). To move the file to a new path, use the move method instead. Adapter implementations will forward this to a move call in most cases.

Signature:

public function rename(string $path, string $newName): void

Possible errors:

  • Entity Not Found. Will be thrown if the entity does not exist.
  • Entity Exists. Will be thrown if an entity exists at the target path.
  • Entity Is Not Accessible. Will be thrown if the entity is not accessible due to insufficient permissions, or other OS-level errors.
  • Root Violation. Will be thrown if the entity is located outside of the configured file
    system root.

Copy

Copies a file or directory from the source path to the destination path. Directories will be copied recursively, if the underlying file system supports it.

Signature:

public function copy(string $sourcePath, string $destinationPath): void

Possible errors:

  • Entity Not Found. Will be thrown if the source entity does not exist.
  • Entity Exists. Will be thrown if an entity exists at the target path.
  • Entity Is Not A Directory. Will be thrown if the parent segment of the target path
    is not a directory.
  • Entity Is Not Accessible. Will be thrown if the entity is not accessible due to insufficient permissions, or other OS-level errors.
  • Root Violation. Will be thrown if the entity is located outside of the configured file
    system root.

Move

Moves a file or directory from the source path to the destination path.

Signature:

public function move(string $sourcePath, string $destinationPath): void

Possible errors:

  • Entity Not Found. Will be thrown if the source entity does not exist.
  • Entity Is A Directory. Will be thrown if the source entity is a file and the target entity is a directory.
  • Entity Is Not A Directory. Will be thrown if the source entity is a directory and the target entity is a file, or the parent segment of the target path is not a directory.
  • Entity Is Not Accessible. Will be thrown if the entity is not accessible due to insufficient permissions, or other OS-level errors.
  • Root Violation. Will be thrown if the entity is located outside of the configured file
    system root.

Delete File

Deletes a file on the file system. This operation will fail for directories, so you should use the deleteDirectory method instead. While it would technically be possible do support both from the same method, prohibiting this was a deliberate design choice: Using the deleteDirectory method makes the intent to delete a full directory absolutely clear, potentially helping to avoid shredding entire directory trees by accident.

Signature:

public function deleteFile(string $path): void

Possible errors:

  • Entity Not Found. Will be thrown if the directory does not exist.
  • Entity Is Not A Directory. Will be thrown if the parent segment of the file path is not a directory.
  • Entity Is Not Accessible. Will be thrown if the file is not accessible due to insufficient permissions, or other OS-level errors.
  • Root Violation. Will be thrown if the file is located outside of the configured file
    system root.

Delete Directory

Deletes a directory and all its contents. This may or may not be supported by the underlying file system but adapter implementations should take care to handle this fact transparently.

Signature:

public function deleteDirectory(string $path): void

Possible errors:

Create Directory

Creates a new directory. This may or may not be supported by the underlying file system but adapter implementations should take care to handle this fact transparently.

Signature:

public function createDirectory(string $path): void

Possible errors:

  • Entity Exists. Will be thrown if an entity exists at the target path.
  • Entity Is Not A Directory. Will be thrown if the parent segment of the file path is not a directory.
  • Entity Is Not Accessible. Will be thrown if the directory is not accessible due to insufficient permissions, or other OS-level errors.
  • Root Violation. Will be thrown if the file is located outside of the configured file
    system root.

Get Adapter

Retrieves the adapter instance. This method exists as an escape hatch in case you need to perform an operation not directly supported by FileSystem without breaking the encapsulation. I recommend avoiding this, tho.

Signature:

public function getAdapter(): AdapterInterface

Errors

FileSystem includes a range of exceptions for common file system errors. All of them inherit from a single base exception, which in turn inherits from the FileSystemException. This allows you to handle errors as fine or coarse as needed.

Will be thrown if an entity exists but the current call requires it to not exist.

Will be thrown if an entity is a directory but the current call requires a file.

Will be thrown if an entity is not a directory but the current call requires a directory.

Will be thrown if an entity is not accessible. This might have a wide number of reasons, among them insufficient permissions, network or protocol errors or other OS-level failures.

Will be thrown if an entity cannot be found on the file system.

Will be thrown if a target path resolves to a target outside of the configured file system root. This effectively prevents directory traversal attacks.

File Objects

File objects provide a wrapper around files, their content and associated meta data. They are constructed with lazy getters for content and meta data which are only invoked the first time you call them.

MetaData Objects

Meta data objects provide a set of general meta data that should be common to most storage backends:

  • File size: Size of the file in bytes. Should be 0 for directories.
  • Last modification time: Timestamp of the last modification as an immutable DateTime. Will be null if not applicable.
  • Creation time: Timestamp of creation as an immutable DateTime. Will be null if not applicable.
  • Other meta data: Associative array of additional, adapter-specific meta data.

Caching and Logging

FileSystem includes several extension interfaces for the FileSystem class that add caching or logging as an add-in. To use them, you need a file system instance that implements those interfaces. The default implementation already extends these interfaces, so all you need to add caching (if your DI container doesn't inject it already), is using the setCache(CacheInterface $cache) method, and the same goes for PSR-3 logging.

Available Adapters

From the get-go, only a small number of adapters is supported. This will increase over time as new adapters are added.
If you feel an adapter is missing, please open a PR or an issue.

Included

  • Local disk file systems: Radiergummi\FileSystem\Adapters\LocalAdapter
  • (S)FTP file systems: Radiergummi\FileSystem\Adapters\FtpAdapter
  • AWS S3 file systems: Radiergummi\FileSystem\Adapters\AwsS3Adapter

Creating new Adapters

All adapters must implement the AdapterInterface. The methods define a clear set of parameter and return types, including @throws tags for all known exceptions the method might throw. Make sure to adhere to the reasons laid out above.
For any exception an adapter might throw that doesn't fit in those categories, you should clearly document the behaviour. All exceptions should bubble up for the user to catch, but that requires them to know about it.

Contributing

Send in a PR or open an issue :) The process will be fleshed out after the initial release.

About

A simple and modern filesystem abstraction layer for PHP

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages