diff --git a/src/Jobs/DeleteTenantStorage.php b/src/Jobs/DeleteTenantStorage.php index c0329c50..f31ddbd2 100644 --- a/src/Jobs/DeleteTenantStorage.php +++ b/src/Jobs/DeleteTenantStorage.php @@ -12,6 +12,13 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\File; use Stancl\Tenancy\Contracts\Tenant; +/** + * Only delete the tenant storage if storage path suffixing is enabled + * and the tenant's storage path is different from the central storage path. + * + * This is to prevent accidental deletion of the central storage when + * a tenant's storage path is not properly suffixed. + */ class DeleteTenantStorage implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -22,9 +29,18 @@ class DeleteTenantStorage implements ShouldQueue public function handle(): void { + // Skip storage deletion if path suffixing is disabled + if (config('tenancy.filesystem.suffix_storage_path') === false) { + return; + } + + $centralPath = tenancy()->central(fn () => storage_path()); $path = tenancy()->run($this->tenant, fn () => storage_path()); - if (is_dir($path)) { + // Skip storage deletion if tenant's storage path is the same as central storage path + $tenantPathIsCentral = realpath($path) === realpath($centralPath); + + if (is_dir($path) && ! $tenantPathIsCentral) { File::deleteDirectory($path); } } diff --git a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php index 136b2100..e7749324 100644 --- a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php @@ -206,19 +206,53 @@ test('tenant storage gets created when TenantCreated listens to CreateTenantStor }); test('tenant storage can get deleted after the tenant when DeletingTenant listens to DeleteTenantStorage', function() { - config([ - 'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class], - ]); - Event::listen(DeletingTenant::class, JobPipeline::make([DeleteTenantStorage::class])->send(function (DeletingTenant $event) { return $event->tenant; })->shouldBeQueued(false)->toListener() ); + $centralStoragePath = storage_path(); + tenancy()->initialize(Tenant::create()); + + // FilesystemTenancyBootstrapper not enabled, + // tenant and central storage path is the same, + // the storage deletion will be skipped. + $tenantStoragePath = storage_path(); + expect($tenantStoragePath)->toBe($centralStoragePath); + expect(File::isDirectory($tenantStoragePath))->toBeTrue(); + tenant()->delete(); + + expect(File::isDirectory($tenantStoragePath))->toBeTrue(); + + config([ + 'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class], + 'tenancy.filesystem.suffix_storage_path' => false, + ]); + + tenancy()->initialize(Tenant::create()); + + $tenantStoragePath = storage_path(); + + // FilesystemTenancyBootstrapper enabled, + // but tenant and central storage path is still the same + // because suffix_storage_path is false. + // The storage deletion will be skipped. + expect($tenantStoragePath)->toBe($centralStoragePath); + expect(File::isDirectory($tenantStoragePath))->toBeTrue(); + tenant()->delete(); + + expect(File::isDirectory($tenantStoragePath))->toBeTrue(); + + config([ + 'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class], + 'tenancy.filesystem.suffix_storage_path' => true, + ]); + tenancy()->initialize(Tenant::create()); $tenantStoragePath = storage_path(); + expect($centralStoragePath)->not()->toBe($tenantStoragePath); expect(File::isDirectory($tenantStoragePath))->toBeTrue(); tenant()->delete();