From 1e1e2efb8c77558f1f2d23203fcb45db6e59944a Mon Sep 17 00:00:00 2001 From: lukinovec Date: Thu, 10 Sep 2026 12:09:07 +0200 Subject: [PATCH] Refactor remove symlinks action, add static::$removeNestedDirectories In removeLink(), return early if the symlink doesn't exist. The nested directory deletion is now controlled by the $removeNestedDirectories static property. It's disabled by default. public_path() and dirname($publicPath) are now normalized using realpath() before the nested dir deletion. The delete loop now checks if the directory-to-be-deleted is *inside* the public root instead of checking if it's not equal to to the public root. --- src/Actions/RemoveStorageSymlinksAction.php | 34 ++++++++++++++++----- tests/ActionTest.php | 10 ++++++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/Actions/RemoveStorageSymlinksAction.php b/src/Actions/RemoveStorageSymlinksAction.php index 78b27e74..14964eae 100644 --- a/src/Actions/RemoveStorageSymlinksAction.php +++ b/src/Actions/RemoveStorageSymlinksAction.php @@ -15,6 +15,15 @@ class RemoveStorageSymlinksAction { use DealsWithTenantSymlinks; + /** + * Should the directories created for nested symlinks be removed along with the symlink. + * + * Before enabling this, make sure you understand the removeLink() method and its implications. + * + * @see CreateStorageSymlinksAction + */ + public static bool $removeNestedDirectories = false; + /** * @param Tenant|Collection|LazyCollection $tenants */ @@ -32,21 +41,32 @@ class RemoveStorageSymlinksAction protected function removeLink(string $publicPath, Tenant $tenant): void { + if (! $this->symlinkExists($publicPath)) { + return; + } + $files = app()->make('files'); - if ($this->symlinkExists($publicPath)) { - event(new RemovingStorageSymlink($tenant)); + event(new RemovingStorageSymlink($tenant)); - $files->delete($publicPath); + $files->delete($publicPath); - event(new StorageSymlinkRemoved($tenant)); + event(new StorageSymlinkRemoved($tenant)); + + if (! static::$removeNestedDirectories) { + return; + } + + $publicRoot = realpath(public_path()); + $directory = realpath(dirname($publicPath)); + + if ($publicRoot === false || $directory === false) { + return; } // Remove the directories CreateStorageSymlinksAction created for the symlink // until a non-empty one is reached. - $directory = dirname($publicPath); - - while ($directory !== public_path() && $files->isEmptyDirectory($directory)) { + while (str_starts_with($directory, $publicRoot . DIRECTORY_SEPARATOR) && $files->isEmptyDirectory($directory)) { $files->deleteDirectory($directory); $directory = dirname($directory); diff --git a/tests/ActionTest.php b/tests/ActionTest.php index 4b364603..8d5a8ac1 100644 --- a/tests/ActionTest.php +++ b/tests/ActionTest.php @@ -17,6 +17,12 @@ use Illuminate\Support\Facades\Storage; beforeEach(function () { Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyEnded::class, RevertToCentralContext::class); + + RemoveStorageSymlinksAction::$removeNestedDirectories = false; +}); + +afterEach(function () { + RemoveStorageSymlinksAction::$removeNestedDirectories = false; }); test('create storage symlinks action works', function (string|null $rootOverride, bool $suffixStoragePath) { @@ -207,6 +213,8 @@ test('symlinks of prefixed disks only expose the prefixed directory', function ( }); test('removing a prefixed disk symlink removes the directories created for it', function () { + RemoveStorageSymlinksAction::$removeNestedDirectories = true; + config([ 'tenancy.bootstrappers' => [ FilesystemTenancyBootstrapper::class, @@ -241,6 +249,8 @@ test('removing a prefixed disk symlink removes the directories created for it', }); test('non-empty directories are not removed with the symlink', function () { + RemoveStorageSymlinksAction::$removeNestedDirectories = true; + config([ 'tenancy.bootstrappers' => [ FilesystemTenancyBootstrapper::class,