From 464f951c9b21db054c88270176a75e140815bfef Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 4 Sep 2026 09:42:16 +0200 Subject: [PATCH] Upstream spatie/temporary-directory state of the touched files before 66215b0adc --- src/TemporaryDirectory.php | 215 ++++++++++++++++++++ tests/TemporaryDirectoryTest.php | 334 +++++++++++++++++++++++++++++++ 2 files changed, 549 insertions(+) create mode 100644 src/TemporaryDirectory.php create mode 100644 tests/TemporaryDirectoryTest.php diff --git a/src/TemporaryDirectory.php b/src/TemporaryDirectory.php new file mode 100644 index 0000000..c65d938 --- /dev/null +++ b/src/TemporaryDirectory.php @@ -0,0 +1,215 @@ +location = $this->sanitizePath($location); + } + + public static function make(string $location = ''): static + { + return (new static($location))->create(); + } + + public function create(): static + { + if (empty($this->location)) { + $this->location = $this->getSystemTemporaryDirectory(); + } + + if (empty($this->name)) { + $this->name = mt_rand().'-'.str_replace([' ', '.'], '', microtime()); + } + + if ($this->forceCreate && file_exists($this->getFullPath())) { + $this->deleteDirectory($this->getFullPath()); + } + + if ($this->exists()) { + throw PathAlreadyExists::create($this->getFullPath()); + } + + mkdir($this->getFullPath(), $this->permission, true); + + return $this; + } + + public function force(): static + { + $this->forceCreate = true; + + return $this; + } + + public function permission(int $permission): static + { + $this->permission = $permission; + + return $this; + } + + public function name(string $name): static + { + $this->name = $this->sanitizeName($name); + + return $this; + } + + public function location(string $location): static + { + $this->location = $this->sanitizePath($location); + + return $this; + } + + public function path(string $pathOrFilename = ''): string + { + if (empty($pathOrFilename)) { + return $this->getFullPath(); + } + + $path = $this->getFullPath().DIRECTORY_SEPARATOR.trim($pathOrFilename, '/'); + + $directoryPath = $this->removeFilenameFromPath($path); + + if (! file_exists($directoryPath)) { + mkdir($directoryPath, $this->permission, true); + } + + return $path; + } + + public function empty(): static + { + $this->deleteDirectory($this->getFullPath()); + + mkdir($this->getFullPath(), $this->permission, true); + + return $this; + } + + public function delete(): bool + { + return $this->deleteDirectory($this->getFullPath()); + } + + public function exists(): bool + { + return file_exists($this->getFullPath()); + } + + public function getName(): string + { + return $this->name; + } + + protected function getFullPath(): string + { + return $this->location.(! empty($this->name) ? DIRECTORY_SEPARATOR.$this->name : ''); + } + + protected function isValidDirectoryName(string $directoryName): bool + { + return strpbrk($directoryName, '\\/?%*:|"<>') === false; + } + + protected function getSystemTemporaryDirectory(): string + { + return rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR); + } + + protected function sanitizePath(string $path): string + { + $path = rtrim($path); + + return rtrim($path, DIRECTORY_SEPARATOR); + } + + protected function sanitizeName(string $name): string + { + if (! $this->isValidDirectoryName($name)) { + throw InvalidDirectoryName::create($name); + } + + return trim($name); + } + + protected function removeFilenameFromPath(string $path): string + { + if (! $this->isFilePath($path)) { + return $path; + } + + return substr($path, 0, strrpos($path, DIRECTORY_SEPARATOR)); + } + + protected function isFilePath(string $path): bool + { + return str_contains($path, '.'); + } + + protected function deleteDirectory(string $path): bool + { + try { + if (is_link($path)) { + return unlink($path); + } + + if (! file_exists($path)) { + return true; + } + + if (! is_dir($path)) { + return unlink($path); + } + + foreach (new FilesystemIterator($path) as $item) { + if (! $this->deleteDirectory((string) $item)) { + return false; + } + } + + /* + * By forcing a php garbage collection cycle using gc_collect_cycles() we can ensure + * that the rmdir does not fail due to files still being reserved in memory. + */ + gc_collect_cycles(); + + return rmdir($path); + } catch (Throwable) { + return false; + } + } + + public function deleteWhenDestroyed(bool $deleteWhenDestroyed = true): static + { + $this->deleteWhenDestroyed = $deleteWhenDestroyed; + + return $this; + } + + public function __destruct() + { + if ($this->deleteWhenDestroyed) { + $this->delete(); + } + } +} diff --git a/tests/TemporaryDirectoryTest.php b/tests/TemporaryDirectoryTest.php new file mode 100644 index 0000000..88035fb --- /dev/null +++ b/tests/TemporaryDirectoryTest.php @@ -0,0 +1,334 @@ +temporaryDirectoryFullPath = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$this->temporaryDirectory; + + $this->deleteDirectory($this->testingDirectory); + $this->deleteDirectory($this->temporaryDirectoryFullPath); + } + + /** @test */ + public function it_can_create_a_temporary_directory() + { + $temporaryDirectory = (new TemporaryDirectory())->create(); + + $this->assertDirectoryExists($temporaryDirectory->path()); + } + + /** @test */ + public function it_can_create_a_temporary_directory_with_shorthand_make() + { + $temporaryDirectory = TemporaryDirectory::make(); + + $this->assertDirectoryExists($temporaryDirectory->path()); + } + + /** @test */ + public function it_can_create_a_temporary_directory_with_a_name() + { + $temporaryDirectory = (new TemporaryDirectory()) + ->name($this->temporaryDirectory) + ->create(); + + $this->assertDirectoryExists($temporaryDirectory->path()); + $this->assertDirectoryExists($this->temporaryDirectoryFullPath); + } + + /** @test */ + public function it_does_not_generate_spaces_in_directory_path() + { + $temporaryDirectory = (new TemporaryDirectory())->create(); + + $this->assertEquals(0, substr_count($temporaryDirectory->path(), ' ')); + } + + /** @test */ + public function it_can_create_a_temporary_directory_in_a_custom_location() + { + $temporaryDirectory = (new TemporaryDirectory()) + ->location($this->testingDirectory) + ->name($this->temporaryDirectory) + ->create(); + + $this->assertDirectoryExists($temporaryDirectory->path()); + $this->assertDirectoryExists($this->testingDirectory.DIRECTORY_SEPARATOR.$this->temporaryDirectory); + } + + /** @test */ + public function it_can_create_a_temporary_directory_in_a_custom_location_through_the_constructor() + { + $temporaryDirectory = (new TemporaryDirectory($this->testingDirectory)) + ->name($this->temporaryDirectory) + ->create(); + + $this->assertDirectoryExists($temporaryDirectory->path()); + $this->assertDirectoryExists($this->testingDirectory.DIRECTORY_SEPARATOR.$this->temporaryDirectory); + } + + /** @test */ + public function it_strips_trailing_slashes_from_a_path() + { + $temporaryDirectory = (new TemporaryDirectory()) + ->name($this->temporaryDirectory) + ->create(); + + $testingPath = $temporaryDirectory->path('testing'.DIRECTORY_SEPARATOR); + $this->assertStringEndsNotWith(DIRECTORY_SEPARATOR, $testingPath); + } + + /** @test */ + public function it_strips_trailing_slashes_from_a_location() + { + $temporaryDirectory = (new TemporaryDirectory($this->testingDirectory.DIRECTORY_SEPARATOR)) + ->create(); + + $this->assertStringEndsNotWith(DIRECTORY_SEPARATOR, $temporaryDirectory->path()); + + $temporaryDirectory = (new TemporaryDirectory()) + ->location($this->testingDirectory.DIRECTORY_SEPARATOR) + ->create(); + + $this->assertStringEndsNotWith(DIRECTORY_SEPARATOR, $temporaryDirectory->path()); + } + + /** @test */ + public function by_default_it_will_not_overwrite_an_existing_directory() + { + mkdir($this->temporaryDirectoryFullPath); + + $this->expectException(PathAlreadyExists::class); + + (new TemporaryDirectory()) + ->name($this->temporaryDirectory) + ->create(); + } + + /** @test */ + public function it_will_overwrite_an_existing_directory_when_using_force_create() + { + mkdir($this->temporaryDirectoryFullPath); + + $testFile = $this->temporaryDirectoryFullPath.DIRECTORY_SEPARATOR.'test.txt'; + + touch($testFile); + + $this->assertFileExists($testFile); + + (new TemporaryDirectory()) + ->force() + ->name($this->temporaryDirectory) + ->create(); + + $this->assertDirectoryExists($this->temporaryDirectoryFullPath); + $this->assertFileDoesNotExist($testFile); + } + + /** @test */ + public function it_provides_chainable_create_methods() + { + $temporaryDirectory = (new TemporaryDirectory()) + ->name($this->temporaryDirectory) + ->create(); + + $this->assertInstanceOf(TemporaryDirectory::class, $temporaryDirectory); + + $temporaryDirectory = (new TemporaryDirectory()) + ->name($this->temporaryDirectory) + ->force() + ->create(); + + $this->assertInstanceOf(TemporaryDirectory::class, $temporaryDirectory); + } + + /** @test */ + public function it_can_create_a_subdirectory_in_the_temporary_directory() + { + $temporaryDirectory = (new TemporaryDirectory()) + ->name($this->temporaryDirectory) + ->create(); + + $subdirectory = 'abc'; + $subdirectoryPath = $temporaryDirectory->path($subdirectory); + + $this->assertDirectoryExists($subdirectoryPath); + $this->assertDirectoryExists("{$this->temporaryDirectoryFullPath}/{$subdirectory}"); + } + + /** @test */ + public function it_can_create_a_multiple_subdirectories_in_the_temporary_directory() + { + $temporaryDirectory = (new TemporaryDirectory()) + ->name($this->temporaryDirectory) + ->create(); + + $subdirectories = 'abc/123/xyz'; + $subdirectoryPath = $temporaryDirectory->path($subdirectories); + + $this->assertDirectoryExists($subdirectoryPath); + $this->assertDirectoryExists("{$this->temporaryDirectoryFullPath}/{$subdirectories}"); + } + + /** @test */ + public function it_can_create_a_path_to_a_file_in_the_temporary_directory() + { + $temporaryDirectory = (new TemporaryDirectory()) + ->name($this->temporaryDirectory) + ->create(); + + $subdirectoriesWithFile = 'abc/123/xyz/test.txt'; + $subdirectoryFilePath = $temporaryDirectory->path($subdirectoriesWithFile); + touch($subdirectoryFilePath); + + $this->assertFileExists($subdirectoryFilePath); + $this->assertFileExists("{$this->temporaryDirectoryFullPath}/{$subdirectoriesWithFile}"); + } + + /** @test */ + public function it_can_delete_a_temporary_directory_containing_files() + { + $temporaryDirectory = (new TemporaryDirectory()) + ->name($this->temporaryDirectory) + ->create(); + + $subdirectoriesWithFile = 'abc/123/xyz/test.txt'; + $subdirectoryPath = $temporaryDirectory->path($subdirectoriesWithFile); + touch($subdirectoryPath); + $temporaryDirectory->delete(); + + $this->assertDirectoryDoesNotExist($this->temporaryDirectoryFullPath); + } + + /** @test */ + public function it_can_delete_a_temporary_directory_containing_no_content() + { + $temporaryDirectory = (new TemporaryDirectory()) + ->name($this->temporaryDirectory) + ->create(); + + $temporaryDirectory->delete(); + + $this->assertDirectoryDoesNotExist($this->temporaryDirectoryFullPath); + } + + /** @test */ + public function it_can_delete_a_temporary_directory_containing_broken_symlink() + { + $temporaryDirectory = (new TemporaryDirectory()) + ->name($this->temporaryDirectory) + ->create(); + + symlink( + $temporaryDirectory->path().DIRECTORY_SEPARATOR.'target', + $temporaryDirectory->path().DIRECTORY_SEPARATOR.'link' + ); + + $temporaryDirectory->delete(); + + $this->assertDirectoryDoesNotExist($this->temporaryDirectoryFullPath); + } + + /** @test */ + public function it_can_empty_a_temporary_directory() + { + $temporaryDirectory = (new TemporaryDirectory()) + ->name($this->temporaryDirectory) + ->create(); + + $subdirectoriesWithFile = 'abc/123/xyz/test.txt'; + $subdirectoryPath = $temporaryDirectory->path($subdirectoriesWithFile); + touch($subdirectoryPath); + $temporaryDirectory->empty(); + + $this->assertFileDoesNotExist($this->temporaryDirectoryFullPath.DIRECTORY_SEPARATOR.$subdirectoriesWithFile); + $this->assertDirectoryExists($this->temporaryDirectoryFullPath); + } + + /** @test */ + public function it_throws_exception_on_invalid_name() + { + $this->expectException(InvalidDirectoryName::class); + $this->expectExceptionMessage('The directory name `/` contains invalid characters.'); + $temporaryDirectory = (new TemporaryDirectory()) + ->name('/'); + } + + /** @test */ + public function it_should_return_true_on_deleted_file_is_not_existed() + { + $temporaryDirectory = (new TemporaryDirectory()) + ->delete(); + + $this->assertTrue($temporaryDirectory); + } + + /** @test */ + public function it_exists_function_should_tell_if_directory_exists() + { + $temporaryDirectory = (new TemporaryDirectory()) + ->name($this->temporaryDirectory); + + $this->assertFalse($temporaryDirectory->exists()); + + $temporaryDirectory->create(); + + $this->assertTrue($temporaryDirectory->exists()); + } + + /** @test */ + public function it_can_delete_when_object_is_destroyed() + { + $temporaryDirectory = (new TemporaryDirectory()) + ->name($this->temporaryDirectory) + ->deleteWhenDestroyed() + ->create(); + + $fullPath = $temporaryDirectory->path(); + + $this->assertDirectoryExists($fullPath); + + unset($temporaryDirectory); + $this->assertDirectoryDoesNotExist($fullPath); + } + + protected function deleteDirectory(string $path): bool + { + if (is_link($path)) { + return unlink($path); + } + + if (! file_exists($path)) { + return true; + } + + if (! is_dir($path)) { + return unlink($path); + } + + foreach (new FilesystemIterator($path) as $item) { + if (! $this->deleteDirectory($item)) { + return false; + } + } + + return rmdir($path); + } +}