diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c48d37..bfa5286 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,5 +14,3 @@ jobs: uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@main cs: uses: innmind/github-workflows/.github/workflows/cs.yml@main - with: - php-version: '8.2' diff --git a/.github/workflows/extensive.yml b/.github/workflows/extensive.yml new file mode 100644 index 0000000..257f139 --- /dev/null +++ b/.github/workflows/extensive.yml @@ -0,0 +1,12 @@ +name: Extensive CI + +on: + push: + tags: + - '*' + paths: + - '.github/workflows/extensive.yml' + +jobs: + blackbox: + uses: innmind/github-workflows/.github/workflows/extensive.yml@main diff --git a/.gitignore b/.gitignore index b292da5..a696500 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ composer.lock vendor -.phpunit.result.cache .cache diff --git a/CHANGELOG.md b/CHANGELOG.md index af9fb77..911b0c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## [Unreleased] + +### Added + +- `Innmind\Filesystem\Recover` + +### Changed + +- Requires PHP `8.4` +- Links are not silently ignored when listing files/directories. An error is still returned when trying to remove a link. +- `Innmind\Filesystem\Adapter\Filesystem::mount()` now returns an `Innmind\Immutable\Attempt` +- `Innmind\Filesystem\Directory::removed()` is now flagged as internal +- `Innmind\Filesystem\Adapter` is now a final class +- `Innmind\Filesystem\Adapter\Filesystem` is now flagged as internal +- `Innmind\Filesystem\Adapter\InMemory` is now flagged as internal +- `Innmind\Filesystem\Adapter\Logger` is now flagged as internal +- `Innmind\Filesystem\Adapter::mount()` no longer automatically create the directory if it doesn't exist + +### Removed + +- `Innmind\Filesystem\Adapter\InMemory::new()` +- `Innmind\Filesystem\Adapter\Filesystem::withCaseSensitivity()`, case sensitivity can be specified as the second argument of `::mount()` +- `Innmind\Filesystem\Exception\*` +- `Innmind\Filesystem\File\Content::atPath()`, use `Content::io()` instead + ## 8.1.0 - 2025-05-09 ### Added diff --git a/README.md b/README.md index 9feabc5..ac45b6c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ use Innmind\Filesystem\{ File, File\Content, Directory, - Adapter\Filesystem, + Adapter, }; use Innmind\Url\Path; @@ -34,7 +34,7 @@ $directory = Directory::named('uploads')->add( Content::ofString(\file_get_contents($_FILES['my_upload']['tmp_name'])), ), ); -$adapter = Filesystem::mount(Path::of('/var/www/web/')); +$adapter = Adapter::mount(Path::of('/var/www/web/'))->unwrap(); $_ = $adapter ->add($directory) ->unwrap(); @@ -42,6 +42,5 @@ $_ = $adapter This example show you how you can create a new directory `uploads` in the folder `/var/www/web/` of your filesystem and create the uploaded file into it. -**Note**: For performance reasons the filesystem adapter only persist to disk the files that have changed (achievable via the immutable nature of file objects). - -All adapters implements [`Adapter`](src/Adapter.php), so you can easily replace them; especially for unit tests, that's why the library comes with an [`InMemory`](src/Adapter/InMemory.php) adapter that only keeps the files into memory so you won't mess up your file system. +> [!NOTE] +> For performance reasons the filesystem adapter only persist to disk the files that have changed (achievable via the immutable nature of file objects). diff --git a/blackbox.php b/blackbox.php index a8adaf5..6195716 100644 --- a/blackbox.php +++ b/blackbox.php @@ -12,6 +12,10 @@ Application::new($argv) ->disableMemoryLimit() // because the generated trees can be quite large ->scenariiPerProof(20) + ->when( + \getenv('BLACKBOX_SET_SIZE') !== false, + static fn(Application $app) => $app->scenariiPerProof((int) \getenv('BLACKBOX_SET_SIZE')), + ) ->when( \getenv('ENABLE_COVERAGE') !== false, static fn(Application $app) => $app @@ -26,9 +30,6 @@ ) ->scenariiPerProof(1), ) - ->when( - \method_exists(Application::class, 'allowProofsToNotMakeAnyAssertions'), - static fn($app) => $app->allowProofsToNotMakeAnyAssertions(), - ) + ->allowProofsToNotMakeAnyAssertions() ->tryToProve(Load::everythingIn(__DIR__.'/proofs/')) ->exit(); diff --git a/composer.json b/composer.json index 0434cda..8100d45 100644 --- a/composer.json +++ b/composer.json @@ -15,14 +15,13 @@ "issues": "http://github.com/Innmind/filesystem/issues" }, "require": { - "php": "~8.2", - "innmind/immutable": "~4.15|~5.0", - "symfony/filesystem": "~6.0|~7.0", - "innmind/media-type": "~2.1", - "innmind/url": "~4.2", + "php": "~8.4", + "innmind/immutable": "~6.0", + "innmind/media-type": "~3.0", + "innmind/url": "~5.0", "psr/log": "~3.0", - "innmind/io": "^3.0.1", - "innmind/validation": "~2.0" + "innmind/io": "~4.0", + "innmind/validation": "~3.0" }, "autoload": { "psr-4": { @@ -37,9 +36,10 @@ } }, "require-dev": { - "innmind/static-analysis": "^1.2.1", - "innmind/black-box": "^6.0.2", + "innmind/static-analysis": "~1.3", + "innmind/black-box": "~6.5", "innmind/coding-standard": "~2.0", + "symfony/filesystem": "~6.0|~7.0|~8.0", "ramsey/uuid": "^4.6" }, "conflict": { diff --git a/documentation/case_sensitivity.md b/documentation/case_sensitivity.md index a736b68..52c53d1 100644 --- a/documentation/case_sensitivity.md +++ b/documentation/case_sensitivity.md @@ -12,14 +12,16 @@ If you're dealing with a case insensitive filesystem then you need to specify it ```php use Innmind\Filesystem\{ - Adapter\Filesystem, + Adapter, CaseSensitivity, }; use Innmind\Url\Path; -$adapter = Filesystem::mount(Path::of('somewhere/')) - ->withCaseSensitivity(CaseSensitivity::insensitive); -$adapter instanceof Filesystem; // true, use $adapter as usual +$adapter = Adapter::mount( + Path::of('somewhere/'), + CaseSensitivity::insensitive, +)->unwrap() +$adapter instanceof Adapter; // true, use $adapter as usual ``` !!! tip "" diff --git a/documentation/testing/in_app.md b/documentation/testing/in_app.md index 52349c9..d21617d 100644 --- a/documentation/testing/in_app.md +++ b/documentation/testing/in_app.md @@ -1,12 +1,12 @@ # In your application -In your application you'll use most of the time the `Filesystem` adapter but in your tests writing to the filesystem can be slow. Instead you can use the `InMemory` adapter. +In your application you'll use most of the time the `Adapter::mount()` adapter but in your tests writing to the filesystem can be slow. Instead you can use the `Adapter::inMemory()` one. ```php -use Innmind\Filesystem\Adapter\InMemory; +use Innmind\Filesystem\Adapter; -$filesystem = InMemory::emulateFilesystem(); +$filesystem = Adapter::inMemory(); // use $filesystem as usual ``` -This adapter is tested against the same [properties](own_adapter.md) as `Filesystem` to make sure there is no divergence of behaviour between the two. +This adapter is tested against the same [properties](https://innmind.org/BlackBox/getting-started/property/) as `Adapter::mount()` to make sure there is no divergence of behaviour between the two. diff --git a/documentation/testing/own_adapter.md b/documentation/testing/own_adapter.md deleted file mode 100644 index d1eb406..0000000 --- a/documentation/testing/own_adapter.md +++ /dev/null @@ -1,27 +0,0 @@ -# Your own adapter - -This library allows you to extend its behaviour by creating new implementations of the exposed interface `Adapter`. The interface is strict enough to guide you through the expected behaviour but the type system can't express all of them, leaving the door open to inconsistencies between implementations. That's why the library expose a set of properties (as declared by [`innmind/black-box`](https://packagist.org/packages/innmind/black-box)) to help you make sure your implementations fulfill the expected behaviours. - -You can test properties on your adapter as follow: - -```php -use Properties\Innmind\Filesystem\Adapter; -use Innmind\BlackBox\Set; - -return static function() { - yield properties( - 'YourAdapter', - Adapter::properties(), - Set::call(fn() => /* instanciate YourAdapter here */), - ); - - foreach (Adapter::alwaysApplicable() as $property) { - yield property( - $property, - Set::call(fn() => /* instanciate YourAdapter here */), - )->named('YourAdapter'); - } -}; -``` - -Then you can [run your proofs](https://innmind.github.io/BlackBox/organize/) via BlackBox. diff --git a/documentation/use_cases/backup_directory.md b/documentation/use_cases/backup_directory.md index b409fbd..bcc75ed 100644 --- a/documentation/use_cases/backup_directory.md +++ b/documentation/use_cases/backup_directory.md @@ -1,11 +1,11 @@ # Backup a directory ```php -use Innmind\Filesystem\Adapter\Filesystem; +use Innmind\Filesystem\Adapter; use Innmind\Url\Path; -$source = Filesystem::mount(Path::of('/var/data/')); -$backup = Filesystem::mount(Path::of('/volumes/backup/')); +$source = Adapter::mount(Path::of('/var/data/'))->unwrap(); +$backup = Adapter::mount(Path::of('/volumes/backup/'))->unwrap(); $source ->root() ->foreach(static fn($file) => $backup->add($file)->unwrap()); diff --git a/documentation/use_cases/delete_file.md b/documentation/use_cases/delete_file.md index 30505b1..2fae923 100644 --- a/documentation/use_cases/delete_file.md +++ b/documentation/use_cases/delete_file.md @@ -4,12 +4,12 @@ ```php use Innmind\Filesystem\{ - Adapter\Filesystem, + Adapter, Name, }; use Innmind\Url\Path; -$filesystem = Filesystem::mount(Path::of('/var/data/')); +$filesystem = Adapter::mount(Path::of('/var/data/'))->unwrap(); $_ = $filesystem ->remove(Name::of('some file')) ->unwrap(); @@ -21,14 +21,14 @@ If the file doesn't exist it will do nothing and if the name corresponds to a di ```php use Innmind\Filesystem\{ - Adapter\Filesystem, + Adapter, Name, Directory, }; use Innmind\Url\Path; use Innmind\Immutable\Predicate\Instance; -$filesystem = Filesystem::mount(Path::of('/var/data/')); +$filesystem = Adapter::mount(Path::of('/var/data/'))->unwrap(); $filesystem ->get(Name::of('some directory')) ->keep(Instance::of(Directory::class)) //(1) diff --git a/documentation/use_cases/load_ftp_files.md b/documentation/use_cases/load_ftp_files.md index ee3a802..d70de64 100644 --- a/documentation/use_cases/load_ftp_files.md +++ b/documentation/use_cases/load_ftp_files.md @@ -4,7 +4,7 @@ Say you have a client that push csv files in an unstructured manner inside a FTP ```php use Innmind\Filesystem\{ - Adapter\Filesystem, + Adapter, File, Directory, }; @@ -24,11 +24,12 @@ function flatten(File|Directory $file): Sequence return Sequence::of($file); } -Filesystem::mount(Path::of('/path/to/ftp/directory/')) +Adapter::mount(Path::of('/path/to/ftp/directory/')) + ->unwrap() ->root() ->all() ->flatMap(flatten(...)) ->foreach(static fn(File $csv) => doYourStuff($csv)); ``` -The advantage of this approach is that you can easily test the whole program behaviour by replacing the `Filesystem` adapter by a `InMemory` one. +The advantage of this approach is that you can easily test the whole program behaviour by replacing the `Adapter::mount()` adapter by `Adapter::inMemory()`. diff --git a/documentation/use_cases/modify_file.md b/documentation/use_cases/modify_file.md index 8ebd886..1c8f58d 100644 --- a/documentation/use_cases/modify_file.md +++ b/documentation/use_cases/modify_file.md @@ -7,7 +7,7 @@ ```php use Innmind\Filesystem\{ - Adapter\Filesystem, + Adapter, File, Name, }; @@ -34,8 +34,8 @@ $release = static function(File $changelog) use ($insertRelease): File { ), ); } -$filesystem = Filesystem::mount(Path::of('some/repository/')); -$tmp = Filesystem::mount(Path::of('/tmp/')); +$filesystem = Adapter::mount(Path::of('some/repository/'))->unwrap(); +$tmp = Adapter::mount(Path::of('/tmp/'))->unwrap(); $filesystem ->get(Name::of('CHANGELOG.md')) ->keep(Instance::of(File::class)) @@ -59,7 +59,7 @@ This example modifies the `CHANGELOG.md` file to replace the `## [Unreleased]` t ```php use Innmind\Filesystem\{ - Adapter\Filesystem, + Adapter, File, File\Content, File\Content\Line, @@ -88,8 +88,8 @@ $update = static function(File $users) use ($updateUser): File { static fn($content) => $content->flatMap(static fn($line) => $updateUser($line)), ); }; -$filesystem = Filesystem::mount(Path::of('/var/data/')); -$tmp = Filesystem::mount(Path::of('/tmp/')); +$filesystem = Adapter::mount(Path::of('/var/data/'))->unwrap(); +$tmp = Adapter::mount(Path::of('/tmp/'))->unwrap(); $filesystem ->get(Name::of('users.csv')) ->keep(Instance::of(File::class)) @@ -113,7 +113,7 @@ This example will insert the user `Jane Doe` after `John Doe` wherever he is in ```php use Innmind\Filesystem\{ - Adapter\Filesystem, + Adapter, File, File\Content, Name, @@ -134,7 +134,7 @@ $merge = static function(File $file1, File $file2): File { ), ); }; -$filesystem = Filesystem::mount(Path::of('/var/data/')); +$filesystem = Adapter::mount(Path::of('/var/data/'))->unwrap(); $users1 = $filesystem ->get(Name::of('users1.csv')) ->keep(Instance::of(File::class)); diff --git a/documentation/use_cases/persist_hand_file.md b/documentation/use_cases/persist_hand_file.md index 123ac74..d5cef44 100644 --- a/documentation/use_cases/persist_hand_file.md +++ b/documentation/use_cases/persist_hand_file.md @@ -4,13 +4,13 @@ ```php use Innmind\Filesystem\{ - Adapter\Filesystem, + Adapter, File, File\Content, }; use Innmind\Url\Path; -$filesystem = Filesystem::mount(Path::of('/var/data/')); +$filesystem = Adapter::mount(Path::of('/var/data/'))->unwrap(); $_ = $filesystem ->add(File::named('some name'), Content::none()) ->unwrap(); @@ -22,7 +22,7 @@ This is equivalent of running the cli command `touch '/var/data/some name'`. ```php use Innmind\Filesystem\{ - Adapter\Filesystem, + Adapter, File, File\Content, File\Content\Line, @@ -33,7 +33,7 @@ use Innmind\Immutable\{ Str, }; -$filesystem = Filesystem::mount(Path::of('/var/data/')); +$filesystem = Adapter::mount(Path::of('/var/data/'))->unwrap(); $_ = $filesystem ->add(File::named( 'some name', @@ -52,14 +52,14 @@ When the file is persisted the _end of line_ character will be automatically add ```php use Innmind\Filesystem\{ - Adapter\Filesystem, + Adapter, File, File\Content, Directory, }; use Innmind\Url\Path; -$filesystem = Filesystem::mount(Path::of('/var/data/')); +$filesystem = Adapter::mount(Path::of('/var/data/'))->unwrap(); $_ = $filesystem ->add( Directory::named('whatever')->add( diff --git a/documentation/use_cases/persist_process_output.md b/documentation/use_cases/persist_process_output.md index ae23dc4..423195d 100644 --- a/documentation/use_cases/persist_process_output.md +++ b/documentation/use_cases/persist_process_output.md @@ -6,14 +6,14 @@ This example uses the [`innmind/operating-system`](https://packagist.org/package use Innmind\Filesystem\{ File, File\Content, - Adapter\Filesystem, + Adapter, }; use Innmind\OperatingSystem\Factory; use Innmind\Server\Control\Server\Command; use Innmind\Url\Path; $os = Factory::build(); -$filesystem = Filesystem::mount(Path::of('/var/data/')); +$filesystem = Adapter::mount(Path::of('/var/data/'))->unwrap(); $fileContent = Content::ofChunks( $os ->control() diff --git a/documentation/use_cases/persist_uploaded_file.md b/documentation/use_cases/persist_uploaded_file.md index f96d851..23054aa 100644 --- a/documentation/use_cases/persist_uploaded_file.md +++ b/documentation/use_cases/persist_uploaded_file.md @@ -5,14 +5,14 @@ use Innmind\Filesystem\{ File, File\Content, Directory, - Adapter\Filesystem, + Adapter, Name, }; use Innmind\Url\Path; use Innmind\Immutable\Predicate\Instance; -$tmp = Filesystem::mount(Path::of(\dirname($_FILES['my_upload']['tmp_name']))); -$filesystem = Filesystem::mount(Path::of('/var/data/')); +$tmp = Adapter::mount(Path::of(\dirname($_FILES['my_upload']['tmp_name'])))->unwrap(); +$filesystem = Adapter::mount(Path::of('/var/data/'))->unwrap(); $_ = $tmp ->get(Name::of(\basename($_FILES['my_upload']['tmp_name']))) diff --git a/documentation/use_cases/read_file.md b/documentation/use_cases/read_file.md index 1c4b4c3..398c4d1 100644 --- a/documentation/use_cases/read_file.md +++ b/documentation/use_cases/read_file.md @@ -4,7 +4,7 @@ ```php use Innmind\Filesystem\{ - Adapter\Filesystem, + Adapter, File, Name, }; @@ -19,7 +19,7 @@ $print = static function(File $file): void { }); }; -$filesystem = Filesystem::mount(Path::of('/var/data/')); +$filesystem = Adapter::mount(Path::of('/var/data/'))->unwrap(); $filesystem ->get(Name::of('some file')) ->keep(Instance::of(File::class)) @@ -35,7 +35,7 @@ This example will print each line to the screen, or nothing if the file doesn't ```php use Innmind\Filesystem\{ - Adapter\Filesystem, + Adapter, File, Name, Directory, @@ -51,7 +51,7 @@ $print = static function(File $file): void { }); }; -$filesystem = Filesystem::mount(Path::of('/var/data/')); +$filesystem = Adapter::mount(Path::of('/var/data/'))->unwrap(); $filesystem ->get(Name::of('some directory')) ->keep(Instance::of(Directory::class)) //(1) diff --git a/mkdocs.yml b/mkdocs.yml index 56b3820..829dfa7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,7 +17,6 @@ nav: - Case sensitivity: case_sensitivity.md - Testing: - In your app: testing/in_app.md - - Your own adapter: testing/own_adapter.md theme: name: material diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100644 index 9d3a4b0..0000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - ./tests - - - - - . - - - ./tests - ./vendor - - - diff --git a/proofs/adapter/filesystem.php b/proofs/adapter/filesystem.php index f66a8a0..b6c4c47 100644 --- a/proofs/adapter/filesystem.php +++ b/proofs/adapter/filesystem.php @@ -2,67 +2,115 @@ declare(strict_types = 1); use Innmind\Filesystem\{ - Adapter\Filesystem, + Adapter, Directory, File, File\Content, CaseSensitivity, + Recover, +}; +use Innmind\IO\{ + IO, + Simulation\Disk, }; use Innmind\Url\Path; -use Properties\Innmind\Filesystem\Adapter; +use Properties\Innmind\Filesystem\Adapter as PAdapter; use Innmind\BlackBox\Set; use Symfony\Component\Filesystem\Filesystem as FS; return static function() { + $path = \rtrim(\sys_get_temp_dir(), '/').'/innmind/filesystem/'; + yield properties( 'Filesystem properties', - Adapter::properties(), - Set::call(static function() { - $path = \sys_get_temp_dir().'/innmind/filesystem/'; + PAdapter::properties(), + Set::call(static function() use ($path) { (new FS)->remove($path); - return Filesystem::mount(Path::of($path))->withCaseSensitivity(match (\PHP_OS) { - 'Darwin' => CaseSensitivity::insensitive, - default => CaseSensitivity::sensitive, - }); + return Adapter::mount( + Path::of($path), + match (\PHP_OS) { + 'Darwin' => CaseSensitivity::insensitive, + default => CaseSensitivity::sensitive, + }, + ) + ->recover(Recover::mount(...)) + ->unwrap(); }), ); - foreach (Adapter::alwaysApplicable() as $property) { + foreach (PAdapter::alwaysApplicable() as $property) { yield property( $property, - Set::call(static function() { - $path = \sys_get_temp_dir().'/innmind/filesystem/'; + Set::call(static function() use ($path) { (new FS)->remove($path); - return Filesystem::mount(Path::of($path))->withCaseSensitivity(match (\PHP_OS) { - 'Darwin' => CaseSensitivity::insensitive, - default => CaseSensitivity::sensitive, - }); + return Adapter::mount( + Path::of($path), + match (\PHP_OS) { + 'Darwin' => CaseSensitivity::insensitive, + default => CaseSensitivity::sensitive, + }, + ) + ->recover(Recover::mount(...)) + ->unwrap(); }), )->named('Filesystem'); } yield test( 'Regression adding file in directory due to case sensitivity', - static function($assert) { - $property = new Adapter\AddRemoveAddModificationsStillAddTheFile( + static function($assert) use ($path) { + $property = new PAdapter\AddRemoveAddModificationsStillAddTheFile( Directory::named('0') ->add($file = File::named('L', Content::none())) ->remove($file->name()), File::named('l', Content::none()), ); - $path = \sys_get_temp_dir().'/innmind/filesystem/'; (new FS)->remove($path); - $adapter = Filesystem::mount(Path::of($path))->withCaseSensitivity(match (\PHP_OS) { - 'Darwin' => CaseSensitivity::insensitive, - default => CaseSensitivity::sensitive, - }); + $adapter = Adapter::mount( + Path::of($path), + match (\PHP_OS) { + 'Darwin' => CaseSensitivity::insensitive, + default => CaseSensitivity::sensitive, + }, + ) + ->recover(Recover::mount(...)) + ->unwrap(); $property->ensureHeldBy($assert, $adapter); (new FS)->remove($path); }, ); + + foreach (CaseSensitivity::cases() as $case) { + yield properties( + 'Filesystem properties on simulated disk', + PAdapter::properties(), + Set::call(static fn() => Adapter::mount( + Path::of('/'), + $case, + IO::simulation( + IO::fromAmbientAuthority(), + Disk::new(), + ), + )->unwrap()), + ); + + foreach (PAdapter::alwaysApplicable() as $property) { + yield property( + $property, + Set::call(static fn() => Adapter::mount( + Path::of('/'), + $case, + IO::simulation( + IO::fromAmbientAuthority(), + Disk::new(), + ), + )->unwrap()), + )->named('Filesystem on simulated disk'); + } + } }; diff --git a/proofs/adapter/inMemory.php b/proofs/adapter/inMemory.php index c794480..7ba4045 100644 --- a/proofs/adapter/inMemory.php +++ b/proofs/adapter/inMemory.php @@ -1,30 +1,46 @@ named('InMemory'); + foreach (PAdapter::alwaysApplicable() as $property) { yield property( $property, - Set::call(InMemory::emulateFilesystem(...)), + Set::call(Adapter::inMemory(...)), )->named('InMemory emulating filesystem'); } + + yield test( + 'Adding a file in a directory should not remove other files starting with the same name', + static function($assert) { + $adapter = Adapter::inMemory(); + $property = new PAdapter\AddDirectoryFromAnotherAdapterWithFileAdded( + Name::of('0'), + File::named( + '+1', + Content::none(), + ), + File::named( + '+', + Content::none(), + ), + ); + + $property->ensureHeldBy($assert, $adapter); + }, + ); }; diff --git a/proofs/file/content.php b/proofs/file/content.php index 0b9ff5b..046a468 100644 --- a/proofs/file/content.php +++ b/proofs/file/content.php @@ -26,15 +26,6 @@ static fn($lines) => Model::ofString(\implode("\n", $lines)), ), ], - [ - 'Content::atPath()', - Set::of('LICENSE', 'CHANGELOG.md', 'composer.json') - ->map(Path::of(...)) - ->map(static fn($path) => Model::atPath( - $io, - $path, - )), - ], [ 'Content::io()', Set::of('LICENSE', 'CHANGELOG.md', 'composer.json') @@ -227,7 +218,7 @@ static function($assert) use ($io) { \file_get_contents('LICENSE'), $content ->chunks() - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(), ); }, @@ -262,10 +253,12 @@ static function($assert, $a, $b) use ($io) { yield test( 'Content::ofChunks()->size() does not load the whole file in memory', static function($assert) use ($io) { - $atPath = Model::atPath( - $io, - Path::of('samples/sample.pdf'), - ); + $atPath = $io + ->files() + ->access(Path::of('samples/sample.pdf')) + ->map(static fn($file) => $file->read()) + ->map(Model::io(...)) + ->unwrap(); $content = Model::ofChunks($atPath->chunks()); $assert diff --git a/properties/Adapter/AddFileWithSameNameAsDirectoryDeleteTheDirectory.php b/properties/Adapter/AddFileWithSameNameAsDirectoryDeleteTheDirectory.php index cfa1594..91ffa89 100644 --- a/properties/Adapter/AddFileWithSameNameAsDirectoryDeleteTheDirectory.php +++ b/properties/Adapter/AddFileWithSameNameAsDirectoryDeleteTheDirectory.php @@ -51,7 +51,7 @@ public function applicableTo(object $adapter): bool public function ensureHeldBy(Assert $assert, object $adapter): object { - $adapter->remove($this->file->name())->unwrap(); + $_ = $adapter->remove($this->file->name())->unwrap(); $assert ->object($adapter->add($this->directory)->unwrap()) ->instance(SideEffect::class); diff --git a/properties/Adapter/AddRemoveAddModificationsStillAddTheFile.php b/properties/Adapter/AddRemoveAddModificationsStillAddTheFile.php index 33f484a..2a93d1f 100644 --- a/properties/Adapter/AddRemoveAddModificationsStillAddTheFile.php +++ b/properties/Adapter/AddRemoveAddModificationsStillAddTheFile.php @@ -48,7 +48,7 @@ public function applicableTo(object $adapter): bool public function ensureHeldBy(Assert $assert, object $adapter): object { - $adapter + $_ = $adapter ->add( $this ->directory diff --git a/properties/Adapter/AllRootFilesAreAccessible.php b/properties/Adapter/AllRootFilesAreAccessible.php index 3fe8a37..3c1b2b2 100644 --- a/properties/Adapter/AllRootFilesAreAccessible.php +++ b/properties/Adapter/AllRootFilesAreAccessible.php @@ -30,7 +30,7 @@ public function applicableTo(object $adapter): bool public function ensureHeldBy(Assert $assert, object $adapter): object { - $adapter + $_ = $adapter ->root() ->foreach(static function($file) use ($assert, $adapter) { $assert->true($adapter->contains($file->name())); diff --git a/properties/Adapter/ReAddingFilesHasNoSideEffect.php b/properties/Adapter/ReAddingFilesHasNoSideEffect.php index fd14a52..cd8daa2 100644 --- a/properties/Adapter/ReAddingFilesHasNoSideEffect.php +++ b/properties/Adapter/ReAddingFilesHasNoSideEffect.php @@ -30,10 +30,10 @@ public function applicableTo(object $adapter): bool public function ensureHeldBy(Assert $assert, object $adapter): object { - $adapter + $_ = $adapter ->root() ->foreach(static function($file) use ($assert, $adapter) { - $adapter->add($file)->unwrap(); + $_ = $adapter->add($file)->unwrap(); $assert->true($adapter->contains($file->name())); if ($file instanceof Directory) { diff --git a/properties/Adapter/RemoveAddRemoveModificationsDoesntAddTheFile.php b/properties/Adapter/RemoveAddRemoveModificationsDoesntAddTheFile.php index 5a74719..f7b0603 100644 --- a/properties/Adapter/RemoveAddRemoveModificationsDoesntAddTheFile.php +++ b/properties/Adapter/RemoveAddRemoveModificationsDoesntAddTheFile.php @@ -53,7 +53,7 @@ public function applicableTo(object $adapter): bool public function ensureHeldBy(Assert $assert, object $adapter): object { - $adapter + $_ = $adapter ->add( $this ->directory diff --git a/properties/Content/Chunks.php b/properties/Content/Chunks.php index 9b02dd0..69e0bc2 100644 --- a/properties/Content/Chunks.php +++ b/properties/Content/Chunks.php @@ -32,7 +32,7 @@ public function ensureHeldBy(Assert $assert, object $systemUnderTest): object $systemUnderTest->toString(), $systemUnderTest ->chunks() - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(), ); $assert diff --git a/properties/Content/Lines.php b/properties/Content/Lines.php index 53a0a65..f9fef07 100644 --- a/properties/Content/Lines.php +++ b/properties/Content/Lines.php @@ -28,7 +28,7 @@ public function applicableTo(object $systemUnderTest): bool public function ensureHeldBy(Assert $assert, object $systemUnderTest): object { $content = $assert->string($systemUnderTest->toString()); - $systemUnderTest + $_ = $systemUnderTest ->lines() ->foreach(static fn($line) => $content->contains($line->toString())); diff --git a/properties/Content/Size.php b/properties/Content/Size.php index 9a7a786..ea86aa9 100644 --- a/properties/Content/Size.php +++ b/properties/Content/Size.php @@ -28,12 +28,13 @@ public function applicableTo(object $systemUnderTest): bool public function ensureHeldBy(Assert $assert, object $systemUnderTest): object { $expected = \mb_strlen($systemUnderTest->toString(), 'ascii'); - $systemUnderTest + $size = $systemUnderTest ->size() ->match( - static fn($size) => $assert->same($expected, $size->toInt()), + static fn($size) => $size->toInt(), static fn() => null, ); + $assert->same($expected, $size); return $systemUnderTest; } diff --git a/properties/Directory/ThrowWhenFlatMappingToSameFileTwice.php b/properties/Directory/ThrowWhenFlatMappingToSameFileTwice.php index 9bd1d10..4805bee 100644 --- a/properties/Directory/ThrowWhenFlatMappingToSameFileTwice.php +++ b/properties/Directory/ThrowWhenFlatMappingToSameFileTwice.php @@ -7,7 +7,6 @@ File, Directory, Name, - Exception\DuplicatedFile, }; use Innmind\Immutable\Sequence; use Innmind\BlackBox\{ @@ -71,7 +70,7 @@ public function ensureHeldBy(Assert $assert, object $directory): object } catch (\Exception $e) { $assert ->object($e) - ->instance(DuplicatedFile::class); + ->instance(\LogicException::class); } return $directory; diff --git a/properties/Directory/ThrowWhenMappingToSameFileTwice.php b/properties/Directory/ThrowWhenMappingToSameFileTwice.php index b61a87a..d1d382d 100644 --- a/properties/Directory/ThrowWhenMappingToSameFileTwice.php +++ b/properties/Directory/ThrowWhenMappingToSameFileTwice.php @@ -6,7 +6,6 @@ use Innmind\Filesystem\{ Directory, File, - Exception\DuplicatedFile, }; use Innmind\BlackBox\{ Property, @@ -54,7 +53,7 @@ public function ensureHeldBy(Assert $assert, object $directory): object } catch (\Exception $e) { $assert ->object($e) - ->instance(DuplicatedFile::class); + ->instance(\LogicException::class); } return $directory; diff --git a/src/Adapter.php b/src/Adapter.php index 01a9711..53a9c21 100644 --- a/src/Adapter.php +++ b/src/Adapter.php @@ -3,31 +3,223 @@ namespace Innmind\Filesystem; +use Innmind\Filesystem\{ + Adapter\Name as Name_, + Adapter\TreePath, + Adapter\Implementation, + Adapter\Filesystem, + Adapter\InMemory, + Adapter\Logger, + Exception\MountPathDoesntExist, + Exception\RecoverMount, +}; +use Innmind\IO\IO; +use Innmind\Url\Path; use Innmind\Immutable\{ - Maybe, Attempt, + Maybe, + Set, SideEffect, }; +use Psr\Log\LoggerInterface; /** * Layer between value objects and concrete implementation */ -interface Adapter +final class Adapter { + /** @var \WeakMap */ + private \WeakMap $loaded; + + private function __construct( + private Implementation $implementation, + private CaseSensitivity $case, + ) { + /** @var \WeakMap */ + $this->loaded = new \WeakMap; + } + + /** + * @return Attempt + */ + public static function mount( + Path $path, + CaseSensitivity $case = CaseSensitivity::sensitive, + ?IO $io = null, + ): Attempt { + return Filesystem::mount($path, $io) + ->map(static fn($implementation) => new self( + $implementation, + $case, + )) + ->mapError(static fn($e) => match (true) { + $e instanceof MountPathDoesntExist => new RecoverMount( + static fn() => $e + ->recover() + ->map(static fn($implementation) => new self( + $implementation, + $case, + )), + ), + default => $e, + }); + } + + public static function inMemory(): self + { + return new self( + InMemory::emulateFilesystem(), + CaseSensitivity::sensitive, + ); + } + + public static function logger( + self $adapter, + LoggerInterface $logger, + ): self { + return new self( + Logger::psr($adapter->implementation, $logger), + $adapter->case, + ); + } + + /** + * @return Attempt + */ + public function add(File|Directory $file): Attempt + { + return $this->write(TreePath::root(), $file); + } + + /** + * @return Maybe + */ + public function get(Name $file): Maybe + { + return $this->access( + TreePath::root(), + Name_\Unknown::of($file), + ); + } + + public function contains(Name $file): bool + { + return $this->implementation->exists(TreePath::of($file))->match( + static fn($exists) => $exists, + static fn() => false, + ); + } + /** * @return Attempt */ - public function add(File|Directory $file): Attempt; + public function remove(Name $file): Attempt + { + return $this->implementation->remove(TreePath::root(), $file); + } + + public function root(): Directory + { + $root = TreePath::root(); + + return Directory::named( + 'root', + $this + ->implementation + ->list($root) + ->map(fn($name) => $this->access($root, $name)) + ->flatMap(static fn($read) => $read->toSequence()), + ); + } /** * @return Maybe */ - public function get(Name $file): Maybe; - public function contains(Name $file): bool; + private function access( + TreePath $path, + Name_\File|Name_\Directory|Name_\Unknown $name, + ): Maybe { + $fullPath = TreePath::of($name->unwrap())->under($path); + + return $this + ->implementation + ->access($path, $name) + ->maybe() + ->map(fn($file) => match (true) { + $file instanceof File => $file, + default => Directory::of( + $file->unwrap(), + $this + ->implementation + ->list(TreePath::directory($name->unwrap())->under($path)) + ->map(fn($file) => $this->access( + $fullPath, + $file, + )) + ->flatMap(static fn($read) => $read->toSequence()), + ), + }) + ->map(function($file) use ($fullPath) { + $this->loaded[$file] = $fullPath; + + return $file; + }); + } /** * @return Attempt */ - public function remove(Name $file): Attempt; - public function root(): Directory; + private function write(TreePath $path, File|Directory $file): Attempt + { + $fullPath = TreePath::of($file)->under($path); + + /** @psalm-suppress PossiblyNullReference */ + if ( + $this->loaded->offsetExists($file) && + $this->loaded[$file]->equals($fullPath) + ) { + // no need to persist untouched file where it was loaded from + return Attempt::result(SideEffect::identity); + } + + $this->loaded[$file] = $fullPath; + + if ($file instanceof Directory) { + /** @var Set */ + $names = Set::of(); + + return $this + ->implementation + ->createDirectory($path, $file->name()) + ->flatMap( + fn() => $file + ->all() + ->sink($names) + ->attempt( + fn($persisted, $file) => $this + ->write($fullPath, $file) + ->map(static fn() => ($persisted)($file->name())), + ), + ) + ->flatMap( + fn($persisted) => $file + ->removed() + ->exclude(fn($file): bool => $this->case->contains( + $file, + $persisted, + )) + ->unsorted() + ->sink(SideEffect::identity) + ->attempt(fn($_, $file) => $this->implementation->remove($fullPath, $file)), + ); + } + + return $this + ->implementation + ->remove($path, $file->name()) + ->flatMap(fn() => $this->implementation->write( + $path, + $file, + )); + } } diff --git a/src/Adapter/Filesystem.php b/src/Adapter/Filesystem.php index cf01b47..633247c 100644 --- a/src/Adapter/Filesystem.php +++ b/src/Adapter/Filesystem.php @@ -4,263 +4,223 @@ namespace Innmind\Filesystem\Adapter; use Innmind\Filesystem\{ - Adapter, + Adapter\Name as Name_, File, + File\Content, Name, - Directory, - CaseSensitivity, - Exception\PathDoesntRepresentADirectory, - Exception\PathTooLong, - Exception\LinksAreNotSupported, + Exception\MountPathDoesntExist, +}; +use Innmind\IO\{ + IO, + Files, }; -use Innmind\IO\IO; use Innmind\MediaType\MediaType; use Innmind\Url\Path; use Innmind\Immutable\{ Sequence, Str, - Maybe, Attempt, SideEffect, - Set, }; -use Symfony\Component\Filesystem\Filesystem as FS; -final class Filesystem implements Adapter +final class Filesystem implements Implementation { - private const INVALID_FILES = ['.', '..']; - private IO $io; - private Path $path; - private CaseSensitivity $case; - private FS $filesystem; - /** @var \WeakMap */ - private \WeakMap $loaded; - private function __construct( - IO $io, - Path $path, - CaseSensitivity $case, + private IO $io, + private Path $path, ) { - if (!$path->directory()) { - throw new PathDoesntRepresentADirectory($path->toString()); - } - - $this->io = $io; - $this->path = $path; - $this->case = $case; - $this->filesystem = new FS; - /** @var \WeakMap */ - $this->loaded = new \WeakMap; - - if (!$this->filesystem->exists($this->path->toString())) { - $this->filesystem->mkdir($this->path->toString()); - } } + /** + * @return Attempt + */ public static function mount( Path $path, ?IO $io = null, - ): self { - return new self( - $io ?? IO::fromAmbientAuthority(), - $path, - CaseSensitivity::sensitive, - ); - } + ): Attempt { + if (!$path->directory()) { + return Attempt::error(new \LogicException(\sprintf( + "Path doesn't represent a directory '%s'", + $path->toString(), + ))); + } - public function withCaseSensitivity(CaseSensitivity $case): self - { - return new self($this->io, $this->path, $case); + $io ??= IO::fromAmbientAuthority(); + + return self::assert($path) + ->map($io->files()->exists(...)) + ->flatMap(static fn($exist) => match ($exist) { + false => Attempt::error(new MountPathDoesntExist( + static fn() => $io + ->files() + ->create($path) + ->map(static fn() => new self( + $io, + $path, + )), + )), + default => Attempt::result(SideEffect::identity), + }) + ->map(static fn() => new self( + $io, + $path, + )); } #[\Override] - public function add(File|Directory $file): Attempt + public function exists(TreePath $path): Attempt { - return $this->createFileAt($this->path, $file); + return self::assert($path->asPath($this->path))->map( + $this->io->files()->exists(...), + ); } #[\Override] - public function get(Name $file): Maybe - { - if (!$this->contains($file)) { - /** @var Maybe */ - return Maybe::nothing(); + public function access( + TreePath $parent, + Name_\File|Name_\Directory|Name_\Unknown $name, + ): Attempt { + if ($name instanceof Name_\Directory) { + return Attempt::result($name); } - return Maybe::just($this->open($this->path, $file)); + $name = $name->unwrap(); + $path = TreePath::of($name) + ->under($parent) + ->asPath($this->path); + + return self::assert($path) + ->flatMap($this->io->files()->access(...)) + ->flatMap(static fn($file) => match (true) { + $file instanceof Files\Link => Attempt::error(new \RuntimeException('Links are not supported')), + default => Attempt::result($file), + }) + ->map(static fn($file) => match (true) { + $file instanceof Files\Directory => Name_\Directory::of($name), + default => File::of( + $name, + Content::io($file->read()), + $file + ->mediaType() + ->maybe() + ->flatMap(MediaType::maybe(...)) + ->match( + static fn($mediaType) => $mediaType, + static fn() => null, + ), + ), + }); } #[\Override] - public function contains(Name $file): bool + public function list(TreePath $parent): Sequence { - return $this->filesystem->exists($this->path->toString().'/'.$file->toString()); + return $this + ->io + ->files() + ->access($parent->asPath($this->path)) + ->flatMap(static fn($file) => match (true) { + $file instanceof Files\Directory => Attempt::result($file), + default => Attempt::error(new \RuntimeException('Path is not a directory')), + }) + ->unwrap() // todo silently return an empty sequence ? + ->list() + ->map(static fn($name) => match ($name->kind()) { + Files\Kind::directory => Name_\Directory::of(Name::of($name->toString())), + default => Name_\File::of(Name::of($name->toString())), // let the read function handle the links + }); } + /** + * This method only relies on the returned boolean to know if the deletion + * was successful or not. It doesn't check afterward if the content is no + * longer there as it may lead to race conditions with other processes. + * + * Such race condition could be P1 removes a file, P2 creates the same file + * and then P1 check the file doesn't exist. This scenario would report a + * failure. + * + * This package doesn't want to bleed this global state between processes. + * If you end up here, know that you should design your app in a way that + * there is as little as possible race conditions like these. + */ #[\Override] - public function remove(Name $file): Attempt + public function remove(TreePath $parent, Name $name): Attempt { - return Attempt::of( - fn() => $this->filesystem->remove( - $this->path->toString().'/'.$file->toString(), - ), - )->map(static fn() => SideEffect::identity()); + $path = TreePath::of($name) + ->under($parent) + ->asPath($this->path); + + return self::assert($path) + ->map($this->io->files()->access(...)) + ->flatMap(static fn($file) => $file->eitherWay( + static fn($file) => match (true) { + $file instanceof Files\Link => Attempt::error(new \RuntimeException('Links are not supported')), + default => $file->remove(), + }, + static fn() => Attempt::result(SideEffect::identity), + )); } #[\Override] - public function root(): Directory + public function createDirectory(TreePath $parent, Name $name): Attempt { - return Directory::lazy( - Name::of('root'), - $this->list($this->path), - ); + $path = TreePath::directory($name) + ->under($parent) + ->asPath($this->path); + + return self::assert($path) + ->map($this->io->files()->access(...)) + ->flatMap(fn($file) => $file->eitherWay( + fn($file) => match (true) { + $file instanceof Files\Link => Attempt::error(new \RuntimeException('Links are not supported')), + $file instanceof Files\Directory => Attempt::result($file), + default => $this + ->remove($parent, $name) + ->flatMap( + fn() => $this + ->io + ->files() + ->create($path), + ), + }, + fn() => $this + ->io + ->files() + ->create($path), + )) + ->flatMap(static fn($file) => match (true) { + $file instanceof Files\Directory => Attempt::result(SideEffect::identity), + default => Attempt::error(new \RuntimeException('File created instead of a directory')), + }); } - /** - * Create the wished file at the given absolute path - * - * @return Attempt - */ - private function createFileAt(Path $path, File|Directory $file): Attempt + #[\Override] + public function write(TreePath $parent, File $file): Attempt { - $name = $file->name()->toString(); - - if ($file instanceof Directory) { - $name .= '/'; - } - - $path = $path->resolve(Path::of($name)); - - /** @psalm-suppress PossiblyNullReference */ - if ($this->loaded->offsetExists($file) && $this->loaded[$file]->equals($path)) { - // no need to persist untouched file where it was loaded from - return Attempt::result(SideEffect::identity()); - } - - $this->loaded[$file] = $path; - - if ($file instanceof Directory) { - /** @var Set */ - $names = Set::of(); - - return Attempt::of( - fn() => $this->filesystem->mkdir($path->toString()), - ) - ->flatMap( - fn() => $file - ->all() - ->sink($names) - ->attempt( - fn($persisted, $file) => $this - ->createFileAt($path, $file) - ->map(static fn() => ($persisted)($file->name())), - ), - ) - ->flatMap( - fn($persisted) => $file - ->removed() - ->filter(fn($file): bool => !$this->case->contains( - $file, - $persisted, - )) - ->unsorted() - ->sink(null) - ->attempt( - fn($_, $file) => Attempt::of( - fn() => $this->filesystem->remove( - $path->toString().$file->toString(), - ), - ), - ) - ->map(static fn() => SideEffect::identity()), - ); - } - - if (\is_dir($path->toString())) { - try { - $this->filesystem->remove($path->toString()); - } catch (\Throwable $e) { - return Attempt::error($e); - } - } - + $absolutePath = TreePath::of($file)->under($parent)->asPath($this->path); $chunks = $file->content()->chunks(); - try { - $this->filesystem->touch($path->toString()); - } catch (\Throwable $e) { - if (\PHP_OS === 'Darwin' && Str::of($path->toString(), Str\Encoding::ascii)->length() > 1014) { - return Attempt::error(new PathTooLong($path->toString(), 0, $e)); - } - - return Attempt::error($e); - } - - return $this - ->io - ->files() - ->write($path) - ->watch() - ->sink($chunks); + return self::assert($absolutePath) + ->flatMap($this->io->files()->create(...)) + ->flatMap(static fn($file) => match (true) { + $file instanceof Files\Directory => Attempt::error(new \RuntimeException('Directory created instead of a file')), + default => $file + ->write() + ->watch() + ->sink($chunks), + }); } /** - * Open the file in the given folder + * @return Attempt */ - private function open(Path $folder, Name $file): File|Directory + private static function assert(Path $path): Attempt { - $path = $folder->resolve(Path::of($file->toString())); - - if (\is_dir($path->toString())) { - $directoryPath = $folder->resolve(Path::of($file->toString().'/')); - $files = $this->list($directoryPath); - - $directory = Directory::lazy($file, $files); - $this->loaded[$directory] = $directoryPath; - - return $directory; + if (Str::of($path->toString())->length() > \PHP_MAXPATHLEN) { + return Attempt::error(new \RuntimeException('Path too long')); } - if (\is_link($path->toString())) { - throw new LinksAreNotSupported($path->toString()); - } - - $file = File::of( - $file, - File\Content::atPath( - $this->io, - $path, - ), - MediaType::maybe(match ($mediaType = @\mime_content_type($path->toString())) { - false => '', - default => $mediaType, - })->match( - static fn($mediaType) => $mediaType, - static fn() => MediaType::null(), - ), - ); - $this->loaded[$file] = $path; - - return $file; - } - - /** - * @return Sequence - */ - private function list(Path $path): Sequence - { - /** @var Sequence */ - return Sequence::lazy(function() use ($path): \Generator { - $files = new \FilesystemIterator($path->toString()); - - /** @var \SplFileInfo $file */ - foreach ($files as $file) { - if (\is_link($file->getPathname())) { - throw new LinksAreNotSupported($file->getPathname()); - } - - /** @psalm-suppress ArgumentTypeCoercion */ - yield $this->open($path, Name::of($file->getBasename())); - } - }); + return Attempt::result($path); } } diff --git a/src/Adapter/Implementation.php b/src/Adapter/Implementation.php new file mode 100644 index 0000000..5139275 --- /dev/null +++ b/src/Adapter/Implementation.php @@ -0,0 +1,54 @@ + + */ + public function exists(TreePath $path): Attempt; + + /** + * @return Attempt + */ + public function access( + TreePath $parent, + Name_\File|Name_\Directory|Name_\Unknown $name, + ): Attempt; + + /** + * @return Sequence + */ + public function list(TreePath $parent): Sequence; + + /** + * @return Attempt + */ + public function remove(TreePath $parent, Name $name): Attempt; + + /** + * @return Attempt + */ + public function createDirectory(TreePath $parent, Name $name): Attempt; + + /** + * @return Attempt + */ + public function write(TreePath $parent, File $file): Attempt; +} diff --git a/src/Adapter/InMemory.php b/src/Adapter/InMemory.php index 112746f..04d9895 100644 --- a/src/Adapter/InMemory.php +++ b/src/Adapter/InMemory.php @@ -4,106 +4,171 @@ namespace Innmind\Filesystem\Adapter; use Innmind\Filesystem\{ - Adapter, + Adapter\Name as Name_, File, Name, - Directory, }; +use Innmind\Url\Path; use Innmind\Immutable\{ - Maybe, + Str, + Map, Attempt, + Sequence, SideEffect, - Predicate\Instance, }; -final class InMemory implements Adapter +/** + * @internal + */ +final class InMemory implements Implementation { - private Directory $root; - - private function __construct() - { - $this->root = Directory::named('root'); - } - /** - * @deprecated Use self::emulateFilesystem() + * @param Map $files + * @param Map> $directories */ - public static function new(): self - { - return self::emulateFilesystem(); + private function __construct( + private Map $files, + private Map $directories, + ) { } public static function emulateFilesystem(): self { - return new self; + return new self( + Map::of(), + Map::of(), + ); } #[\Override] - public function add(File|Directory $file): Attempt + public function exists(TreePath $path): Attempt { - $this->root = $this->merge($this->root, $file); + $path = $this->path($path); - return Attempt::result(SideEffect::identity()); + return Attempt::result($this->files->contains($path) || $this->directories->contains("$path/")); } #[\Override] - public function get(Name $file): Maybe - { - return $this->root->get($file); + public function access( + TreePath $parent, + Name_\File|Name_\Directory|Name_\Unknown $name, + ): Attempt { + if ($name instanceof Name_\Directory) { + return $this + ->directories + ->get($this->path(TreePath::directory($name->unwrap())->under($parent))) + ->map(static fn() => $name) + ->attempt(static fn() => new \RuntimeException('Directory not found')); + } + + if ($name instanceof Name_\File) { + return $this + ->files + ->get($this->path(TreePath::of($name->unwrap())->under($parent))) + ->attempt(static fn() => new \RuntimeException('File not found')); + } + + return $this + ->access($parent, Name_\Directory::of($name->unwrap())) + ->recover(fn() => $this->access( + $parent, + Name_\File::of($name->unwrap()), + )); } #[\Override] - public function contains(Name $file): bool + public function list(TreePath $parent): Sequence { - return $this->root->contains($file); + $path = $this->path($parent); + + /** @psalm-suppress ArgumentTypeCoercion Due to Name::of() */ + return $this + ->directories + ->get($path) + ->toSequence() + ->flatMap(static fn($files) => $files) + ->map(fn($file) => match ($this->directories->contains("$path$file/")) { + true => Name_\Directory::of(Name::of($file)), + false => Name_\File::of(Name::of($file)), + }); } #[\Override] - public function remove(Name $file): Attempt + public function remove(TreePath $parent, Name $name): Attempt { - $this->root = $this->root->remove($file); + $asDirectory = $this->path(TreePath::directory($name)->under($parent)); + $this->files = $this + ->files + ->remove($this->path(TreePath::of($name)->under($parent))) + ->exclude(static fn($path) => Str::of($path)->startsWith($asDirectory)); + $parent = $this->path($parent); + $directories = $this + ->directories + ->exclude(static fn($path) => Str::of($path)->startsWith($asDirectory)); + $files = $directories + ->get($parent) + ->toSequence() + ->flatMap(static fn($files) => $files) + ->exclude(static fn($file) => $file === $name->toString()); + $this->directories = ($directories)( + $parent, + $files, + ); - return Attempt::result(SideEffect::identity()); + return Attempt::result(SideEffect::identity); } #[\Override] - public function root(): Directory + public function createDirectory(TreePath $parent, Name $name): Attempt { - return $this->root; - } + $path = $this->path(TreePath::directory($name)->under($parent)); + $asFile = Str::of($path) + ->dropEnd(1) // trailing / + ->toString(); - private function merge(Directory $parent, File|Directory $file): Directory - { - if (!$file instanceof Directory) { - return $parent->add($file); - } + $this->files = $this->files->remove($asFile); - $file = $parent - ->get($file->name()) - ->keep(Instance::of(Directory::class)) - ->match( - fn($existing) => $this->mergeDirectories($existing, $file), - static fn() => $file, + if (!$this->directories->contains($path)) { + $this->directories = ($this->directories)( + $path, + Sequence::strings(), ); + $parent = $this->path($parent); + $files = $this + ->directories + ->get($parent) + ->toSequence() + ->flatMap(static fn($files) => $files) + ->add($name->toString()); + $this->directories = ($this->directories)( + $parent, + $files, + ); + } - return $parent->add($file); + return Attempt::result(SideEffect::identity); } - private function mergeDirectories( - Directory $existing, - Directory $new, - ): Directory { - $existing = $new - ->removed() - ->filter(static fn($name) => !$new->contains($name)) - ->reduce( - $existing, - static fn(Directory $existing, $name) => $existing->remove($name), - ); + #[\Override] + public function write(TreePath $parent, File $file): Attempt + { + $fullPath = $this->path(TreePath::of($file)->under($parent)); + $parent = $this->path($parent); + + $this->files = ($this->files)($fullPath, $file); + $files = $this + ->directories + ->get($parent) + ->toSequence() + ->flatMap(static fn($files) => $files) + ->add($file->name()->toString()); + $this->directories = ($this->directories)($parent, $files); + + return Attempt::result(SideEffect::identity); + } - return $new->reduce( - $existing, - fn(Directory $directory, $file) => $this->merge($directory, $file), - ); + private function path(TreePath $path): string + { + return $path->asPath(Path::of('/'))->toString(); } } diff --git a/src/Adapter/Logger.php b/src/Adapter/Logger.php index 3f3f2c1..13e68dd 100644 --- a/src/Adapter/Logger.php +++ b/src/Adapter/Logger.php @@ -4,80 +4,124 @@ namespace Innmind\Filesystem\Adapter; use Innmind\Filesystem\{ - Adapter, + Adapter\Name as Name_, File, Name, - Directory, }; +use Innmind\Url\Path; use Innmind\Immutable\{ - Maybe, + Sequence, Attempt, }; use Psr\Log\LoggerInterface; -final class Logger implements Adapter +/** + * @internal + */ +final class Logger implements Implementation { - private Adapter $filesystem; - private LoggerInterface $logger; - - private function __construct(Adapter $filesystem, LoggerInterface $logger) - { - $this->filesystem = $filesystem; - $this->logger = $logger; + private function __construct( + private Implementation $implementation, + private LoggerInterface $logger, + ) { } - public static function psr(Adapter $filesystem, LoggerInterface $logger): self + public static function psr(Implementation $implementation, LoggerInterface $logger): self { - return new self($filesystem, $logger); + return new self($implementation, $logger); } #[\Override] - public function add(File|Directory $file): Attempt + public function exists(TreePath $path): Attempt { - $this->logger->debug('Adding file {file}', ['file' => $file->name()->toString()]); + return $this + ->implementation + ->exists($path) + ->map(function($exists) use ($path) { + $this->logger->debug('Cheking if filesystem contains {file}', [ + 'file' => self::path($path), + 'contains' => $exists, + ]); - return $this->filesystem->add($file); + return $exists; + }); } #[\Override] - public function get(Name $file): Maybe - { + public function access( + TreePath $parent, + Name_\File|Name_\Directory|Name_\Unknown $name, + ): Attempt { return $this - ->filesystem - ->get($file) - ->map(function($file) { - $this->logger->debug( - 'Accessing file {name}', - ['name' => $file->name()->toString()], - ); + ->implementation + ->access($parent, $name) + ->map(function($file) use ($parent, $name) { + $this->logger->debug('Accessing file {file}', [ + 'file' => self::path(TreePath::of($name->unwrap())->under($parent)), + ]); return $file; }); } #[\Override] - public function contains(Name $file): bool + public function list(TreePath $parent): Sequence { - $contains = $this->filesystem->contains($file); - $this->logger->debug('Cheking if filesystem contains {file}', [ - 'file' => $file->toString(), - 'contains' => $contains, + $this->logger->debug('Listing files in {directory}', [ + 'directory' => self::path($parent), ]); - return $contains; + return $this->implementation->list($parent); + } + + #[\Override] + public function remove(TreePath $parent, Name $name): Attempt + { + return $this + ->implementation + ->remove($parent, $name) + ->map(function($_) use ($parent, $name) { + $this->logger->debug('File removed {file}', [ + 'file' => self::path(TreePath::of($name)->under($parent)), + ]); + + return $_; + }); } #[\Override] - public function remove(Name $file): Attempt + public function createDirectory(TreePath $parent, Name $name): Attempt { - $this->logger->debug('Removing file {file}', ['file' => $file->toString()]); + return $this + ->implementation + ->createDirectory($parent, $name) + ->map(function($_) use ($parent, $name) { + $this->logger->debug('Directory created {directory}', [ + 'directory' => self::path(TreePath::directory($name)->under($parent)), + ]); - return $this->filesystem->remove($file); + return $_; + }); } #[\Override] - public function root(): Directory + public function write(TreePath $parent, File $file): Attempt + { + return $this + ->implementation + ->write($parent, $file) + ->map(function($_) use ($parent, $file) { + $this->logger->debug('File written {file}', [ + 'file' => self::path(TreePath::of($file)->under($parent)), + 'mediaType' => $file->mediaType()->toString(), + ]); + + return $_; + }); + } + + private static function path(TreePath $path): string { - return $this->filesystem->root(); + return $path->asPath(Path::of('/'))->toString(); } } diff --git a/src/Adapter/Name/Directory.php b/src/Adapter/Name/Directory.php new file mode 100644 index 0000000..c933d23 --- /dev/null +++ b/src/Adapter/Name/Directory.php @@ -0,0 +1,31 @@ +name; + } +} diff --git a/src/Adapter/Name/File.php b/src/Adapter/Name/File.php new file mode 100644 index 0000000..80b7fe3 --- /dev/null +++ b/src/Adapter/Name/File.php @@ -0,0 +1,31 @@ +name; + } +} diff --git a/src/Adapter/Name/Unknown.php b/src/Adapter/Name/Unknown.php new file mode 100644 index 0000000..7f9e45f --- /dev/null +++ b/src/Adapter/Name/Unknown.php @@ -0,0 +1,31 @@ +name; + } +} diff --git a/src/Adapter/TreePath.php b/src/Adapter/TreePath.php new file mode 100644 index 0000000..8660ebb --- /dev/null +++ b/src/Adapter/TreePath.php @@ -0,0 +1,101 @@ + $path + */ + private function __construct( + private Sequence $path, + private bool $directory, + ) { + } + + /** + * @psalm-pure + */ + public static function of(Name|File|Directory $file): self + { + return new self( + Sequence::of(match (true) { + $file instanceof Name => $file, + default => $file->name(), + }), + $file instanceof Directory, + ); + } + + /** + * @psalm-pure + */ + public static function directory(Name $file): self + { + return new self( + Sequence::of($file), + true, + ); + } + + /** + * @psalm-pure + */ + public static function root(): self + { + return new self( + Sequence::of(), + true, + ); + } + + public function under(self $parent): self + { + return new self( + $this->path->append($parent->path), + $this->directory, + ); + } + + public function equals(self $other): bool + { + $root = Path::of('/'); + + return $this->asPath($root)->equals($other->asPath($root)); + } + + public function asPath(Path $root): Path + { + if ($this->path->empty()) { + return $root; + } + + $path = $this + ->path + ->reverse() + ->map(static fn($name) => $name->str()->append('/')) + ->fold(Concat::monoid); + + if (!$this->directory) { + $path = $path->dropEnd(1); // remove trailing '/' + } + + return $root->resolve(Path::of($path->toString())); + } +} diff --git a/src/Directory.php b/src/Directory.php index 5638458..63ce2ca 100644 --- a/src/Directory.php +++ b/src/Directory.php @@ -3,7 +3,6 @@ namespace Innmind\Filesystem; -use Innmind\Filesystem\Exception\DuplicatedFile; use Innmind\Immutable\{ Set, Sequence, @@ -16,29 +15,21 @@ */ final class Directory { - private Name $name; - /** @var Sequence */ - private Sequence $files; - /** @var Set */ - private Set $removed; - /** * @param Sequence $files * @param Set $removed */ - private function __construct(Name $name, Sequence $files, Set $removed) - { - $this->name = $name; - $this->files = $files; - $this->removed = $removed; + private function __construct( + private Name $name, + private Sequence $files, + private Set $removed, + ) { } /** * @psalm-pure * * @param Sequence|null $files - * - * @throws DuplicatedFile */ public static function of(Name $name, ?Sequence $files = null): self { @@ -54,8 +45,6 @@ public static function of(Name $name, ?Sequence $files = null): self * * @param non-empty-string $name * @param Sequence|null $files - * - * @throws DuplicatedFile */ public static function named(string $name, ?Sequence $files = null): self { @@ -102,7 +91,7 @@ public function add(File|self $file): self $this->name, $this ->files - ->filter(static fn(File|self $known): bool => !$known->name()->equals($file->name())) + ->exclude(static fn(File|self $known): bool => $known->name()->equals($file->name())) ->add($file), $this->removed, ); @@ -128,7 +117,7 @@ public function remove(Name $name): self { return new self( $this->name, - $this->files->filter(static fn(File|self $file) => !$file->name()->equals($name)), + $this->files->exclude(static fn(File|self $file) => $file->name()->equals($name)), ($this->removed)($name), ); } @@ -160,8 +149,6 @@ public function filter(callable $predicate): self /** * @param callable(File|self): File $map - * - * @throws DuplicatedFile */ public function map(callable $map): self { @@ -174,8 +161,6 @@ public function map(callable $map): self /** * @param callable(File|self): self $map - * - * @throws DuplicatedFile */ public function flatMap(callable $map): self { @@ -206,6 +191,8 @@ public function reduce($carry, callable $reducer) * This method should only be used for implementations of the Adapter * interface, normal users should never have to use this method * + * @internal + * * @return Set */ public function removed(): Set @@ -226,8 +213,6 @@ public function all(): Sequence * * @param Sequence $files * - * @throws DuplicatedFile - * * @return Sequence */ private static function safeguard(Sequence $files): Sequence @@ -235,7 +220,10 @@ private static function safeguard(Sequence $files): Sequence return $files->safeguard( Set::strings(), static fn(Set $names, $file) => match ($names->contains($file->name()->toString())) { - true => throw new DuplicatedFile($file->name()), + true => throw new \LogicException(\sprintf( + "Same file '%s' found multiple times", + $file->name()->toString(), + )), false => ($names)($file->name()->toString()), }, ); diff --git a/src/Exception/CannotPersistClosedStream.php b/src/Exception/CannotPersistClosedStream.php deleted file mode 100644 index fbace93..0000000 --- a/src/Exception/CannotPersistClosedStream.php +++ /dev/null @@ -1,8 +0,0 @@ -toString()}' found multiple times"); - } -} diff --git a/src/Exception/Exception.php b/src/Exception/Exception.php deleted file mode 100644 index 549d910..0000000 --- a/src/Exception/Exception.php +++ /dev/null @@ -1,8 +0,0 @@ - $recover + */ + public function __construct( + private \Closure $recover, + ) { + } + + /** + * @return Attempt + */ + public function recover(): Attempt + { + return ($this->recover)(); + } +} diff --git a/src/Exception/PathDoesntRepresentADirectory.php b/src/Exception/PathDoesntRepresentADirectory.php deleted file mode 100644 index 116ac28..0000000 --- a/src/Exception/PathDoesntRepresentADirectory.php +++ /dev/null @@ -1,8 +0,0 @@ - $recover + */ + public function __construct( + private \Closure $recover, + ) { + } + + /** + * @return Attempt + */ + public function recover(): Attempt + { + return ($this->recover)(); + } +} diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php deleted file mode 100644 index 8e9c8de..0000000 --- a/src/Exception/RuntimeException.php +++ /dev/null @@ -1,8 +0,0 @@ -name = $name; - $this->content = $content; - $this->mediaType = $mediaType ?? MediaType::null(); } /** @@ -33,7 +26,7 @@ public static function of( Content $content, ?MediaType $mediaType = null, ): self { - return new self($name, $content, $mediaType); + return new self($name, $content, $mediaType ?? MediaType::null()); } /** @@ -46,7 +39,7 @@ public static function named( Content $content, ?MediaType $mediaType = null, ): self { - return new self(Name::of($name), $content, $mediaType); + return self::of(Name::of($name), $content, $mediaType); } public function name(): Name diff --git a/src/File/Content.php b/src/File/Content.php index f60b1aa..22e0ba0 100644 --- a/src/File/Content.php +++ b/src/File/Content.php @@ -8,12 +8,10 @@ Line, }; use Innmind\IO\{ - IO, Streams\Stream, Files\Read, Stream\Size, }; -use Innmind\Url\Path; use Innmind\Immutable\{ Str, Sequence, @@ -26,21 +24,8 @@ */ final class Content { - private Implementation $implementation; - - private function __construct(Implementation $implementation) + private function __construct(private Implementation $implementation) { - $this->implementation = $implementation; - } - - /** - * @psalm-pure - */ - public static function atPath( - IO $io, - Path $path, - ): self { - return new self(Content\AtPath::of($io, $path)); } /** diff --git a/src/File/Content/AtPath.php b/src/File/Content/AtPath.php deleted file mode 100644 index 57181d0..0000000 --- a/src/File/Content/AtPath.php +++ /dev/null @@ -1,109 +0,0 @@ -lines()->foreach($function); - } - - #[\Override] - public function map(callable $map): Implementation - { - return Lines::of($this->lines()->map($map)); - } - - #[\Override] - public function flatMap(callable $map): Implementation - { - return Lines::of($this->lines())->flatMap($map); - } - - #[\Override] - public function filter(callable $filter): Implementation - { - return Lines::of($this->lines()->filter($filter)); - } - - #[\Override] - public function lines(): Sequence - { - /** @psalm-suppress ImpureMethodCall */ - return $this - ->io - ->files() - ->read($this->path) - ->watch() - ->lines() - ->map(Line::fromStream(...)); - } - - #[\Override] - public function reduce($carry, callable $reducer) - { - return $this->lines()->reduce($carry, $reducer); - } - - #[\Override] - public function size(): Maybe - { - /** @psalm-suppress ImpureMethodCall */ - return $this - ->io - ->files() - ->read($this->path) - ->size(); - } - - #[\Override] - public function toString(): string - { - return $this - ->chunks() - ->fold(new Concat) - ->toString(); - } - - #[\Override] - public function chunks(): Sequence - { - /** @psalm-suppress ImpureMethodCall */ - return $this - ->io - ->files() - ->read($this->path) - ->watch() - ->chunks(8192); - } -} diff --git a/src/File/Content/Chunks.php b/src/File/Content/Chunks.php index ea829aa..071ced7 100644 --- a/src/File/Content/Chunks.php +++ b/src/File/Content/Chunks.php @@ -19,15 +19,11 @@ */ final class Chunks implements Implementation { - /** @var Sequence */ - private Sequence $chunks; - /** * @param Sequence $chunks */ - private function __construct(Sequence $chunks) + private function __construct(private Sequence $chunks) { - $this->chunks = $chunks->pad(1, Str::of('')); } /** @@ -37,7 +33,7 @@ private function __construct(Sequence $chunks) */ public static function of(Sequence $chunks): self { - return new self($chunks); + return new self($chunks->pad(1, Str::of(''))); } #[\Override] @@ -114,7 +110,7 @@ public function toString(): string { return $this ->chunks - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(); } diff --git a/src/File/Content/IO.php b/src/File/Content/IO.php index a46c54e..635989b 100644 --- a/src/File/Content/IO.php +++ b/src/File/Content/IO.php @@ -105,7 +105,7 @@ public function toString(): string { return $this ->chunks() - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(); } diff --git a/src/File/Content/Line.php b/src/File/Content/Line.php index f4c350f..5617680 100644 --- a/src/File/Content/Line.php +++ b/src/File/Content/Line.php @@ -3,7 +3,6 @@ namespace Innmind\Filesystem\File\Content; -use Innmind\Filesystem\Exception\DomainException; use Innmind\Immutable\Str; /** @@ -11,11 +10,8 @@ */ final class Line { - private Str $content; - - private function __construct(Str $content) + private function __construct(private Str $content) { - $this->content = $content; } /** @@ -24,7 +20,7 @@ private function __construct(Str $content) public static function of(Str $content): self { if ($content->contains("\n")) { - throw new DomainException('New line delimiter should not appear in the line content'); + throw new \DomainException('New line delimiter should not appear in the line content'); } return new self($content); diff --git a/src/File/Content/Lines.php b/src/File/Content/Lines.php index a932372..46d374a 100644 --- a/src/File/Content/Lines.php +++ b/src/File/Content/Lines.php @@ -17,15 +17,11 @@ */ final class Lines implements Implementation { - /** @var Sequence */ - private Sequence $lines; - /** * @param Sequence $lines */ - private function __construct(Sequence $lines) + private function __construct(private Sequence $lines) { - $this->lines = $lines->pad(1, Line::of(Str::of(''))); } /** @@ -35,7 +31,7 @@ private function __construct(Sequence $lines) */ public static function of(Sequence $lines): self { - return new self($lines); + return new self($lines->pad(1, Line::of(Str::of('')))); } #[\Override] @@ -53,7 +49,7 @@ public function map(callable $map): Implementation #[\Override] public function flatMap(callable $map): Implementation { - return new self($this->lines->flatMap( + return self::of($this->lines->flatMap( static fn($line) => $map($line)->lines(), )); } @@ -61,7 +57,7 @@ public function flatMap(callable $map): Implementation #[\Override] public function filter(callable $filter): Implementation { - return new self($this->lines->filter($filter)); + return self::of($this->lines->filter($filter)); } #[\Override] diff --git a/src/File/Content/OneShot.php b/src/File/Content/OneShot.php index 69509ec..3b5e98c 100644 --- a/src/File/Content/OneShot.php +++ b/src/File/Content/OneShot.php @@ -3,7 +3,6 @@ namespace Innmind\Filesystem\File\Content; -use Innmind\Filesystem\Exception\LogicException; use Innmind\IO\{ Streams\Stream, Frame, @@ -21,12 +20,10 @@ */ final class OneShot implements Implementation { - private Stream $io; private bool $loaded = false; - private function __construct(Stream $io) + private function __construct(private Stream $io) { - $this->io = $io; } /** @@ -97,7 +94,7 @@ public function toString(): string { return $this ->chunks() - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(); } @@ -120,7 +117,7 @@ public function chunks(): Sequence private function guard(): void { if ($this->loaded) { - throw new LogicException("Content can't be loaded twice"); + throw new \LogicException("Content can't be loaded twice"); } /** @psalm-suppress InaccessibleProperty */ diff --git a/src/Name.php b/src/Name.php index 513bfa8..e285f1a 100644 --- a/src/Name.php +++ b/src/Name.php @@ -3,7 +3,6 @@ namespace Innmind\Filesystem; -use Innmind\Filesystem\Exception\DomainException; use Innmind\Immutable\Str; /** @@ -20,29 +19,29 @@ final class Name private function __construct(string $value) { if (Str::of($value)->empty()) { - throw new DomainException('A file name can\'t be empty'); + throw new \DomainException('A file name can\'t be empty'); } if (Str::of($value, Str\Encoding::ascii)->length() > 255) { - throw new DomainException($value); + throw new \DomainException($value); } if (Str::of($value)->contains('/')) { - throw new DomainException("A file name can't contain a slash, $value given"); + throw new \DomainException("A file name can't contain a slash, $value given"); } if (Str::of($value)->contains(\chr(0))) { - throw new DomainException("A file name can't contain the null control character, $value given"); + throw new \DomainException("A file name can't contain the null control character, $value given"); } // name with only _spaces_ are not accepted as it is not as valid path if (Str::of($value)->matches('~^\s+$~')) { - throw new DomainException($value); + throw new \DomainException($value); } if ($value === '.' || $value === '..') { // as they are special links on unix filesystems - throw new DomainException("'.' and '..' can't be used"); + throw new \DomainException("'.' and '..' can't be used"); } $this->value = $value; @@ -53,7 +52,7 @@ private function __construct(string $value) * * @param non-empty-string $value * - * @throws DomainException + * @throws \DomainException */ public static function of(string $value): self { diff --git a/src/Recover.php b/src/Recover.php new file mode 100644 index 0000000..ee183e1 --- /dev/null +++ b/src/Recover.php @@ -0,0 +1,25 @@ + + */ + public static function mount(\Throwable $e): Attempt + { + return match (true) { + $e instanceof RecoverMount => $e->recover(), + default => Attempt::error($e), + }; + } +} diff --git a/tests/Adapter/FilesystemTest.php b/tests/Adapter/FilesystemTest.php index 9284e79..423e380 100644 --- a/tests/Adapter/FilesystemTest.php +++ b/tests/Adapter/FilesystemTest.php @@ -4,16 +4,13 @@ namespace Tests\Innmind\Filesystem\Adapter; use Innmind\Filesystem\{ - Adapter\Filesystem, Adapter, File, File\Content, Name, Directory as DirectoryInterface, Directory, - Exception\PathDoesntRepresentADirectory, - Exception\PathTooLong, - Exception\LinksAreNotSupported, + Recover, }; use Innmind\Url\Path; use Innmind\Immutable\{ @@ -42,7 +39,7 @@ public function setUp(): void public function testInterface() { - $adapter = Filesystem::mount(Path::of('/tmp/')); + $adapter = Adapter::mount(Path::of('/tmp/'))->unwrap(); $this->assertInstanceOf(Adapter::class, $adapter); $this->assertFalse($adapter->contains(Name::of('foo'))); @@ -64,16 +61,17 @@ public function testInterface() public function testThrowWhenPathToMountIsNotADirectory() { - $this->expectException(PathDoesntRepresentADirectory::class); - $this->expectExceptionMessage('path/to/somewhere'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage("Path doesn't represent a directory 'path/to/somewhere'"); - Filesystem::mount(Path::of('path/to/somewhere')); + $_ = Adapter::mount(Path::of('path/to/somewhere'))->unwrap(); } public function testReturnNothingWhenGettingUnknownFile() { $this->assertNull( - Filesystem::mount(Path::of('/tmp/')) + Adapter::mount(Path::of('/tmp/')) + ->unwrap() ->get(Name::of('foo')) ->match( static fn($file) => $file, @@ -86,7 +84,8 @@ public function testRemovingUnknownFileDoesntThrow() { $this->assertInstanceOf( SideEffect::class, - Filesystem::mount(Path::of('/tmp/')) + Adapter::mount(Path::of('/tmp/')) + ->unwrap() ->remove(Name::of('foo')) ->unwrap(), ); @@ -94,7 +93,7 @@ public function testRemovingUnknownFileDoesntThrow() public function testCreateNestedStructure() { - $adapter = Filesystem::mount(Path::of('/tmp/')); + $adapter = Adapter::mount(Path::of('/tmp/'))->unwrap(); $directory = Directory::of(Name::of('foo')) ->add(File::of(Name::of('foo.md'), Content::ofString('# Foo'))) @@ -102,7 +101,7 @@ public function testCreateNestedStructure() Directory::of(Name::of('bar')) ->add(File::of(Name::of('bar.md'), Content::ofString('# Bar'))), ); - $adapter->add($directory)->unwrap(); + $_ = $adapter->add($directory)->unwrap(); $this->assertSame( '# Foo', $adapter @@ -127,7 +126,7 @@ public function testCreateNestedStructure() ), ); - $adapter = Filesystem::mount(Path::of('/tmp/')); + $adapter = Adapter::mount(Path::of('/tmp/'))->unwrap(); $this->assertTrue($adapter->contains(Name::of('foo'))); $this->assertSame( '# Foo', @@ -159,59 +158,59 @@ public function testCreateNestedStructure() ), ); - $adapter + $_ = $adapter ->remove(Name::of('foo')) ->unwrap(); } public function testRemoveFileWhenRemovedFromFolder() { - $a = Filesystem::mount(Path::of('/tmp/')); + $a = Adapter::mount(Path::of('/tmp/'))->unwrap(); $d = Directory::of(Name::of('foo')); $d = $d->add(File::of(Name::of('bar'), Content::ofString('some content'))); - $a->add($d)->unwrap(); + $_ = $a->add($d)->unwrap(); $d = $d->remove(Name::of('bar')); - $a->add($d)->unwrap(); - $this->assertSame(1, $d->removed()->count()); - $a = Filesystem::mount(Path::of('/tmp/')); + $_ = $a->add($d)->unwrap(); + $this->assertSame(1, $d->removed()->size()); + $a = Adapter::mount(Path::of('/tmp/'))->unwrap(); $this->assertFalse( $a->get(Name::of('foo'))->match( static fn($dir) => $dir->contains(Name::of('bar')), static fn() => true, ), ); - $a + $_ = $a ->remove(Name::of('foo')) ->unwrap(); } public function testDoesntFailWhenAddindSameDirectoryTwiceThatContainsARemovedFile() { - $a = Filesystem::mount(Path::of('/tmp/')); + $a = Adapter::mount(Path::of('/tmp/'))->unwrap(); $d = Directory::of(Name::of('foo')); $d = $d->add(File::of(Name::of('bar'), Content::ofString('some content'))); - $a->add($d)->unwrap(); + $_ = $a->add($d)->unwrap(); $d = $d->remove(Name::of('bar')); - $a->add($d)->unwrap(); - $a->add($d)->unwrap(); - $this->assertSame(1, $d->removed()->count()); - $a = Filesystem::mount(Path::of('/tmp/')); + $_ = $a->add($d)->unwrap(); + $_ = $a->add($d)->unwrap(); + $this->assertSame(1, $d->removed()->size()); + $a = Adapter::mount(Path::of('/tmp/'))->unwrap(); $this->assertFalse( $a->get(Name::of('foo'))->match( static fn($dir) => $dir->contains(Name::of('bar')), static fn() => true, ), ); - $a + $_ = $a ->remove(Name::of('foo')) ->unwrap(); } public function testLoadWithMediaType() { - $a = Filesystem::mount(Path::of('/tmp/')); + $a = Adapter::mount(Path::of('/tmp/'))->unwrap(); \file_put_contents( '/tmp/some_content.html', '', @@ -227,15 +226,17 @@ public function testLoadWithMediaType() static fn() => null, ), ); - $a + $_ = $a ->remove(Name::of('some_content.html')) ->unwrap(); } public function testRoot() { - $adapter = Filesystem::mount(Path::of('/tmp/test/')); - $adapter + $adapter = Adapter::mount(Path::of('/tmp/test/')) + ->recover(Recover::mount(...)) + ->unwrap(); + $_ = $adapter ->add(File::of( Name::of('foo'), Content::ofString('foo'), @@ -246,7 +247,7 @@ public function testRoot() \file_put_contents('/tmp/test/baz/foobar', 'baz'); $all = $adapter->root()->all(); - $this->assertCount(3, $all); + $this->assertSame(3, $all->size()); $all = Map::of( ...$all ->map(static fn($file) => [$file->name()->toString(), $file]) @@ -282,20 +283,22 @@ public function testRoot() static fn() => null, ), ); - $adapter + $_ = $adapter ->remove(Name::of('foo')) ->unwrap(); - $adapter + $_ = $adapter ->remove(Name::of('bar')) ->unwrap(); - $adapter + $_ = $adapter ->remove(Name::of('baz')) ->unwrap(); } public function testAddingTheSameFileTwiceDoesNothing() { - $adapter = Filesystem::mount(Path::of('/tmp/')); + $adapter = Adapter::mount(Path::of('/tmp/')) + ->recover(Recover::mount(...)) + ->unwrap(); $file = File::of( Name::of('foo'), Content::ofString('foo'), @@ -324,11 +327,14 @@ public function testPathTooLongThrowAnException() $path = \sys_get_temp_dir().'/innmind/filesystem/'; (new FS)->remove($path); - $filesystem = Filesystem::mount(Path::of($path)); + $filesystem = Adapter::mount(Path::of($path)) + ->recover(Recover::mount(...)) + ->unwrap(); - $this->expectException(PathTooLong::class); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Path too long'); - $filesystem->add(Directory::of( + $_ = $filesystem->add(Directory::of( Name::of(\str_repeat('a', 255)), Sequence::of( Directory::of( @@ -363,7 +369,9 @@ public function testPersistedNameCanStartWithAnyAsciiCharacter() $path = \sys_get_temp_dir().'/innmind/filesystem/'; (new FS)->remove($path); - $filesystem = Filesystem::mount(Path::of($path)); + $filesystem = Adapter::mount(Path::of($path)) + ->recover(Recover::mount(...)) + ->unwrap(); $this->assertInstanceOf( SideEffect::class, @@ -396,7 +404,9 @@ public function testPersistedNameCanContainWithAnyAsciiCharacter() $path = \sys_get_temp_dir().'/innmind/filesystem/'; (new FS)->remove($path); - $filesystem = Filesystem::mount(Path::of($path)); + $filesystem = Adapter::mount(Path::of($path)) + ->recover(Recover::mount(...)) + ->unwrap(); $this->assertInstanceOf( SideEffect::class, @@ -431,7 +441,9 @@ public function testPersistedNameCanContainOnlyOneAsciiCharacter() $path = \sys_get_temp_dir().'/innmind/filesystem/'; (new FS)->remove($path); - $filesystem = Filesystem::mount(Path::of($path)); + $filesystem = Adapter::mount(Path::of($path)) + ->recover(Recover::mount(...)) + ->unwrap(); $this->assertInstanceOf( SideEffect::class, @@ -457,12 +469,16 @@ public function testThrowsWhenTryingToGetLink() (new FS)->dumpFile($path.'foo', 'bar'); \symlink($path.'foo', $path.'bar'); - $filesystem = Filesystem::mount(Path::of($path)); - - $this->expectException(LinksAreNotSupported::class); - $this->expectExceptionMessage($path.'bar'); + $filesystem = Adapter::mount(Path::of($path)) + ->recover(Recover::mount(...)) + ->unwrap(); - $filesystem->get(Name::of('bar')); + $this->assertNull( + $filesystem->get(Name::of('bar'))->match( + static fn($file) => $file, + static fn() => null, + ), + ); } public function testThrowsWhenListContainsALink() @@ -472,12 +488,18 @@ public function testThrowsWhenListContainsALink() (new FS)->dumpFile($path.'foo', 'bar'); \symlink($path.'foo', $path.'bar'); - $filesystem = Filesystem::mount(Path::of($path)); - - $this->expectException(LinksAreNotSupported::class); - $this->expectExceptionMessage($path.'bar'); + $filesystem = Adapter::mount(Path::of($path)) + ->recover(Recover::mount(...)) + ->unwrap(); - $filesystem->root()->all()->toList(); + $this->assertSame( + ['foo'], + $filesystem + ->root() + ->all() + ->map(static fn($file) => $file->name()->toString()) + ->toList(), + ); } public function testDotFilesAreListed() @@ -490,7 +512,9 @@ public function testDotFilesAreListed() (new FS)->mkdir($path); \file_put_contents($path.$name, 'bar'); - $filesystem = Filesystem::mount(Path::of($path)); + $filesystem = Adapter::mount(Path::of($path)) + ->recover(Recover::mount(...)) + ->unwrap(); $all = $filesystem->root()->all()->toList(); $this->assertCount(1, $all); diff --git a/tests/Adapter/InMemoryTest.php b/tests/Adapter/InMemoryTest.php index e132ea6..7fe7d86 100644 --- a/tests/Adapter/InMemoryTest.php +++ b/tests/Adapter/InMemoryTest.php @@ -4,7 +4,6 @@ namespace Tests\Innmind\Filesystem\Adapter; use Innmind\Filesystem\{ - Adapter\InMemory, Adapter, Directory, File, @@ -21,7 +20,7 @@ class InMemoryTest extends TestCase { public function testInterface() { - $a = InMemory::new(); + $a = Adapter::inMemory(); $this->assertInstanceOf(Adapter::class, $a); $this->assertFalse($a->contains(Name::of('foo'))); @@ -32,8 +31,8 @@ public function testInterface() ->unwrap(), ); $this->assertTrue($a->contains(Name::of('foo'))); - $this->assertSame( - $d, + $this->assertInstanceOf( + Directory::class, $a->get(Name::of('foo'))->match( static fn($file) => $file, static fn() => null, @@ -50,7 +49,7 @@ public function testInterface() public function testReturnNothingWhenGettingUnknownFile() { - $this->assertNull(InMemory::new()->get(Name::of('foo'))->match( + $this->assertNull(Adapter::inMemory()->get(Name::of('foo'))->match( static fn($file) => $file, static fn() => null, )); @@ -60,7 +59,7 @@ public function testRemovingUnknownFileDoesntThrow() { $this->assertInstanceOf( SideEffect::class, - InMemory::new() + Adapter::inMemory() ->remove(Name::of('foo')) ->unwrap(), ); @@ -68,14 +67,14 @@ public function testRemovingUnknownFileDoesntThrow() public function testRoot() { - $adapter = InMemory::new(); - $adapter + $adapter = Adapter::inMemory(); + $_ = $adapter ->add($foo = File::of( Name::of('foo'), Content::ofString('foo'), )) ->unwrap(); - $adapter + $_ = $adapter ->add($bar = File::of( Name::of('bar'), Content::ofString('bar'), @@ -91,15 +90,15 @@ public function testRoot() public function testEmulateFilesystem() { - $adapter = InMemory::emulateFilesystem(); - $adapter->add(Directory::of( + $adapter = Adapter::inMemory(); + $_ = $adapter->add(Directory::of( Name::of('foo'), Sequence::of( Directory::named('bar'), File::named('baz', Content::none()), ), ))->unwrap(); - $adapter->add(Directory::of( + $_ = $adapter->add(Directory::of( Name::of('foo'), Sequence::of( Directory::of( diff --git a/tests/Adapter/LoggerTest.php b/tests/Adapter/LoggerTest.php index ab43fdc..28c9679 100644 --- a/tests/Adapter/LoggerTest.php +++ b/tests/Adapter/LoggerTest.php @@ -5,8 +5,6 @@ use Innmind\Filesystem\{ Adapter, - Adapter\Logger, - Adapter\InMemory, File, File\Content, Name, @@ -21,8 +19,8 @@ public function testInterface() { $this->assertInstanceOf( Adapter::class, - Logger::psr( - InMemory::new(), + Adapter::logger( + Adapter::inMemory(), new NullLogger, ), ); @@ -30,8 +28,8 @@ public function testInterface() public function testAdd() { - $adapter = Logger::psr( - $inner = InMemory::new(), + $adapter = Adapter::logger( + $inner = Adapter::inMemory(), new NullLogger, ); $file = File::of(Name::of('foo'), Content::none()); @@ -47,13 +45,13 @@ public function testAdd() public function testGet() { - $adapter = Logger::psr( - $inner = InMemory::new(), + $adapter = Adapter::logger( + $inner = Adapter::inMemory(), new NullLogger, ); $name = Name::of('foo'); $file = File::of($name, Content::none()); - $inner + $_ = $inner ->add($file) ->unwrap(); @@ -68,12 +66,12 @@ public function testGet() public function testContains() { - $adapter = Logger::psr( - $inner = InMemory::new(), + $adapter = Adapter::logger( + $inner = Adapter::inMemory(), new NullLogger, ); $name = Name::of('foo'); - $inner + $_ = $inner ->add(File::of($name, Content::none())) ->unwrap(); @@ -82,12 +80,12 @@ public function testContains() public function testRemove() { - $adapter = Logger::psr( - $inner = InMemory::new(), + $adapter = Adapter::logger( + $inner = Adapter::inMemory(), new NullLogger, ); $name = Name::of('foo'); - $inner + $_ = $inner ->add(File::of($name, Content::none())) ->unwrap(); @@ -102,15 +100,15 @@ public function testRemove() public function testRoot() { - $adapter = Logger::psr( - $inner = InMemory::new(), + $adapter = Adapter::logger( + $inner = Adapter::inMemory(), new NullLogger, ); $file = File::named( 'watev', Content::none(), ); - $inner + $_ = $inner ->add($file) ->unwrap(); diff --git a/tests/CaseSensitivityTest.php b/tests/CaseSensitivityTest.php index 9954aa6..b413bfa 100644 --- a/tests/CaseSensitivityTest.php +++ b/tests/CaseSensitivityTest.php @@ -19,39 +19,31 @@ class CaseSensitivityTest extends TestCase { use BlackBox; - public function testContains() + public function testContainsSensitive(): BlackBox\Proof { - $this + return $this ->forAll( FName::strings(), - FName::strings(), + Set::sequence(FName::strings()), ) - ->filter(static fn($a, $b) => $a !== $b) - ->then(function($a, $b) { + ->filter(static fn($a, $b) => !\in_array($a, $b, true)) + ->prove(function($a, $b) { $this->assertTrue(CaseSensitivity::sensitive->contains( Name::of($a), ISet::of(Name::of($a)), )); - $this->assertFalse(CaseSensitivity::sensitive->contains( - Name::of($a), - ISet::of(Name::of($b)), - )); - }); - $this - ->forAll( - FName::strings(), - Set::sequence(FName::strings()), - ) - ->filter(static fn($a, $b) => !\in_array($a, $b, true)) - ->then(function($a, $b) { $this->assertFalse(CaseSensitivity::sensitive->contains( Name::of($a), ISet::of(...$b)->map(Name::of(...)), )); }); - $this + } + + public function testContainsInsensitive(): BlackBox\Proof + { + return $this ->forAll(FName::strings()) - ->then(function($a) { + ->prove(function($a) { $this->assertTrue(CaseSensitivity::insensitive->contains( Name::of($a), ISet::of($a)->map(\strtolower(...))->map(Name::of(...)), diff --git a/tests/DirectoryTest.php b/tests/DirectoryTest.php index 1d1fd75..72ce96f 100644 --- a/tests/DirectoryTest.php +++ b/tests/DirectoryTest.php @@ -8,7 +8,6 @@ File, Name, File\Content, - Exception\DuplicatedFile, }; use Innmind\Immutable\{ Set, @@ -51,8 +50,8 @@ public function testAdd() $this->assertInstanceOf(Directory::class, $d2); $this->assertNotSame($d, $d2); $this->assertSame($d->name(), $d2->name()); - $this->assertSame(0, $d->removed()->count()); - $this->assertSame(0, $d2->removed()->count()); + $this->assertSame(0, $d->removed()->size()); + $this->assertSame(0, $d2->removed()->size()); $this->assertFalse($d->contains($file->name())); $this->assertTrue($d2->contains($file->name())); $this->assertSame($file, $d2->get($file->name())->match( @@ -106,8 +105,8 @@ public function testRemove() $this->assertInstanceOf(Directory::class, $d2); $this->assertNotSame($d, $d2); $this->assertSame($d->name(), $d2->name()); - $this->assertSame(0, $d->removed()->count()); - $this->assertSame(1, $d2->removed()->count()); + $this->assertSame(0, $d->removed()->size()); + $this->assertSame(1, $d2->removed()->size()); $this->assertSame( 'bar', $d2 @@ -195,7 +194,7 @@ public function testDirectoryLoadedWithDifferentFilesWithTheSameNameThrows() FName::any(), ) ->then(function($directory, $file) { - $this->expectException(DuplicatedFile::class); + $this->expectException(\LogicException::class); $this->expectExceptionMessage("Same file '{$file->toString()}' found multiple times"); Directory::of( @@ -216,7 +215,7 @@ public function testNamedDirectoryLoadedWithDifferentFilesWithTheSameNameThrows( FName::any(), ) ->then(function($directory, $file) { - $this->expectException(DuplicatedFile::class); + $this->expectException(\LogicException::class); $this->expectExceptionMessage("Same file '{$file->toString()}' found multiple times"); Directory::named( diff --git a/tests/File/Content/LineTest.php b/tests/File/Content/LineTest.php index a88c2b6..992cced 100644 --- a/tests/File/Content/LineTest.php +++ b/tests/File/Content/LineTest.php @@ -3,10 +3,7 @@ namespace Tests\Innmind\Filesystem\File\Content; -use Innmind\Filesystem\{ - File\Content\Line, - Exception\DomainException, -}; +use Innmind\Filesystem\File\Content\Line; use Innmind\Immutable\Str; use Innmind\BlackBox\{ PHPUnit\BlackBox, @@ -31,7 +28,7 @@ public function testDoesntAcceptNewLineDelimiter() $this->fail('it should throw'); } catch (\Exception $e) { - $this->assertInstanceOf(DomainException::class, $e); + $this->assertInstanceOf(\DomainException::class, $e); } }); } @@ -90,7 +87,7 @@ public function testMappedLineCannotContainEndOfLineDelimiter() $this->fail('it should throw'); } catch (\Exception $e) { - $this->assertInstanceOf(DomainException::class, $e); + $this->assertInstanceOf(\DomainException::class, $e); } }); } diff --git a/tests/NameTest.php b/tests/NameTest.php index de20b19..1060ca6 100644 --- a/tests/NameTest.php +++ b/tests/NameTest.php @@ -3,10 +3,7 @@ namespace Innmind\Filesystem\Tests; -use Innmind\Filesystem\{ - Name, - Exception\DomainException, -}; +use Innmind\Filesystem\Name; use Innmind\Url\Path; use Innmind\Immutable\Str; use Innmind\BlackBox\{ @@ -29,7 +26,7 @@ public function testInterface() public function testThrowWhenABuildingNameWithASlash() { - $this->expectException(DomainException::class); + $this->expectException(\DomainException::class); $this->expectExceptionMessage('A file name can\'t contain a slash, foo/bar given'); Name::of('foo/bar'); @@ -43,7 +40,7 @@ public function testEquals() public function testEmptyNameIsNotAllowed() { - $this->expectException(DomainException::class); + $this->expectException(\DomainException::class); $this->expectExceptionMessage('A file name can\'t be empty'); Name::of(''); @@ -70,7 +67,7 @@ public function testNameContainingASlashIsNotAccepted() Fixture::strings(), ) ->then(function($a, $b) { - $this->expectException(DomainException::class); + $this->expectException(\DomainException::class); Name::of("$a/$b"); }); @@ -116,7 +113,7 @@ public function testDotFoldersAreNotAccepted() Name::of($name); $this->fail('it should throw'); - } catch (DomainException $e) { + } catch (\DomainException $e) { $this->assertSame("'.' and '..' can't be used", $e->getMessage()); } }); @@ -124,7 +121,7 @@ public function testDotFoldersAreNotAccepted() public function testChr0IsNotAccepted() { - $this->expectException(DomainException::class); + $this->expectException(\DomainException::class); Name::of('a'.\chr(0).'a'); } @@ -150,7 +147,7 @@ public function testNamesLongerThan255AreNotAccepted() $this->fail('it should throw'); } catch (\Throwable $e) { - $this->assertInstanceOf(DomainException::class, $e); + $this->assertInstanceOf(\DomainException::class, $e); } }); } @@ -167,7 +164,7 @@ public function testNameWithOnlyWhiteSpacesIsNotAccepted() Name::of(\chr($ord)); $this->fail('it should throw'); - } catch (DomainException $e) { + } catch (\DomainException $e) { $this->assertTrue(true); } });