From 5ed5aea6d602c84f14d646f242948ac5dcad7574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Vl=C4=8Dek?= Date: Mon, 13 Sep 2021 19:22:06 +0200 Subject: [PATCH] Fixed Link command for Laravel v6, added StorageLink Events, more StorageLink tests, added RemoveStorageSymlinks Job, added Storage Jobs to TenancyServiceProvider stub, renamed misleading config example. --- assets/TenancyServiceProvider.stub.php | 8 +++ assets/config.php | 2 +- src/Commands/Link.php | 78 +++++++++++++++++++++++--- src/Events/CreatingStorageSymlink.php | 9 +++ src/Events/RemovingStorageSymlink.php | 9 +++ src/Events/StorageSymlinkCreated.php | 9 +++ src/Events/StorageSymlinkRemoved.php | 9 +++ src/Jobs/CreateStorageSymlinks.php | 6 ++ src/Jobs/RemoveStorageSymlinks.php | 55 ++++++++++++++++++ tests/BootstrapperTest.php | 50 ++++++++++++++++- tests/CommandsTest.php | 33 ++++++++++- 11 files changed, 253 insertions(+), 15 deletions(-) create mode 100644 src/Events/CreatingStorageSymlink.php create mode 100644 src/Events/RemovingStorageSymlink.php create mode 100644 src/Events/StorageSymlinkCreated.php create mode 100644 src/Events/StorageSymlinkRemoved.php create mode 100644 src/Jobs/RemoveStorageSymlinks.php diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index 1d15f418..6adf6631 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -28,6 +28,7 @@ class TenancyServiceProvider extends ServiceProvider Jobs\CreateDatabase::class, Jobs\MigrateDatabase::class, // Jobs\SeedDatabase::class, + Jobs\CreateStorageSymlinks::class, // Your own jobs to prepare the tenant. // Provision API keys, create S3 buckets, anything you want! @@ -44,6 +45,7 @@ class TenancyServiceProvider extends ServiceProvider Events\TenantDeleted::class => [ JobPipeline::make([ Jobs\DeleteDatabase::class, + Jobs\RemoveStorageSymlinks::class, ])->send(function (Events\TenantDeleted $event) { return $event->tenant; })->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production. @@ -87,6 +89,12 @@ class TenancyServiceProvider extends ServiceProvider Listeners\UpdateSyncedResource::class, ], + // Storage symlinks + Events\CreatingStorageSymlink::class => [], + Events\StorageSymlinkCreated::class => [], + Events\RemovingStorageSymlink::class => [], + Events\StorageSymlinkRemoved::class => [], + // Fired only when a synced resource is changed in a different DB than the origin DB (to avoid infinite loops) Events\SyncedResourceChangedInForeignDatabase::class => [], ]; diff --git a/assets/config.php b/assets/config.php index d175649c..d26a64b9 100644 --- a/assets/config.php +++ b/assets/config.php @@ -123,7 +123,7 @@ return [ */ 'url_override' => [ // The array key is local disk (must exist in root_override) and value is public directory (%tenant_id% will be replaced with actual tenant id). - 'public' => 'storage-%tenant_id%', + 'public' => 'public-%tenant_id%', ], /** diff --git a/src/Commands/Link.php b/src/Commands/Link.php index 5c483728..3aae0a90 100644 --- a/src/Commands/Link.php +++ b/src/Commands/Link.php @@ -4,11 +4,11 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; -use Illuminate\Foundation\Console\StorageLinkCommand; +use Illuminate\Console\Command; use Stancl\Tenancy\Concerns\HasATenantsOption; use Stancl\Tenancy\Contracts\Tenant; -class Link extends StorageLinkCommand +class Link extends Command { use HasATenantsOption; @@ -20,7 +20,8 @@ class Link extends StorageLinkCommand protected $signature = 'tenants:link {--tenants=* : The tenant(s) to run the command for. Default: all} {--relative : Create the symbolic link using relative paths} - {--force : Recreate existing symbolic links}'; + {--force : Recreate existing symbolic links} + {--remove : Remove symbolic links}'; /** * The console command description. @@ -29,6 +30,51 @@ class Link extends StorageLinkCommand */ protected $description = 'Create the symbolic links configured for the tenancy applications'; + /** + * Execute the console command. + * + * @return void + */ + public function handle() + { + $relative = $this->option('relative'); + + if ($this->option('remove')) { + foreach ($this->links() as $link => $target) { + if (is_link($link)) { + $this->laravel->make('files')->delete($link); + + $this->info("The [$link] link has been removed."); + } + } + + $this->info('The links have been removed.'); + + return; + } + + foreach ($this->links() as $link => $target) { + if (file_exists($link) && ! $this->isRemovableSymlink($link, $this->option('force'))) { + $this->error("The [$link] link already exists."); + continue; + } + + if (is_link($link)) { + $this->laravel->make('files')->delete($link); + } + + if ($relative) { + $this->laravel->make('files')->relativeLink($target, $link); + } else { + $this->laravel->make('files')->link($target, $link); + } + + $this->info("The [$link] link has been connected to [$target]."); + } + + $this->info('The links have been created.'); + } + /** * Get the symbolic links that are configured for the application. * @@ -40,16 +86,20 @@ class Link extends StorageLinkCommand $disks = config('tenancy.filesystem.root_override'); $suffix_base = config('tenancy.filesystem.suffix_base'); - return $this->getTenants() - ->map(function (Tenant $tenant) use ($suffix_base, $disk_urls, $disks) { + $tenants = $this->option('remove') && filled($this->option('tenants')) + ? collect($this->option('tenants')) + : $this->getTenants()->map(function(Tenant $tenant) { return $tenant->getTenantKey(); }); + + return $tenants + ->map(function ($tenant_key) use ($suffix_base, $disk_urls, $disks) { $map = []; foreach ($disk_urls as $disk => $public_path) { - $storage_path = str_replace('%storage_path%', $suffix_base . $tenant['id'], $disks[$disk]); + $storage_path = str_replace('%storage_path%', $suffix_base . $tenant_key, $disks[$disk]); $storage_path = storage_path($storage_path); - $public_path = str_replace('%tenant_id%', $tenant['id'], $public_path); + $public_path = str_replace('%tenant_id%', $tenant_key, $public_path); $public_path = public_path($public_path); // make sure storage path exist before we create symlink @@ -63,7 +113,19 @@ class Link extends StorageLinkCommand return $map; })->flatten(1) - ->mapWithKeys(fn ($item) => $item) + ->mapWithKeys(function ($item) {return $item; }) ->all(); } + + /** + * Determine if the provided path is a symlink that can be removed. + * + * @param string $link + * @param bool $force + * @return bool + */ + protected function isRemovableSymlink(string $link, bool $force): bool + { + return is_link($link) && $force; + } } diff --git a/src/Events/CreatingStorageSymlink.php b/src/Events/CreatingStorageSymlink.php new file mode 100644 index 00000000..13937174 --- /dev/null +++ b/src/Events/CreatingStorageSymlink.php @@ -0,0 +1,9 @@ +tenant)); + Artisan::call('tenants:link', [ '--tenants' => [$this->tenant->getTenantKey()], ]); + + event(new StorageSymlinkCreated($this->tenant)); } } diff --git a/src/Jobs/RemoveStorageSymlinks.php b/src/Jobs/RemoveStorageSymlinks.php new file mode 100644 index 00000000..3c22f463 --- /dev/null +++ b/src/Jobs/RemoveStorageSymlinks.php @@ -0,0 +1,55 @@ +tenant = $tenant; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + event(new RemovingStorageSymlink($this->tenant)); + + Artisan::call('tenants:link', [ + '--remove' => true, + '--tenants' => [$this->tenant->getTenantKey()], + ]); + + event(new StorageSymlinkRemoved($this->tenant)); + } +} diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php index cd7e5207..831f3209 100644 --- a/tests/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -17,7 +17,10 @@ use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Events\TenantDeleted; use Stancl\Tenancy\Jobs\CreateDatabase; +use Stancl\Tenancy\Jobs\CreateStorageSymlinks; +use Stancl\Tenancy\Jobs\RemoveStorageSymlinks; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Tests\Etc\Tenant; @@ -218,7 +221,7 @@ class BootstrapperTest extends TestCase FilesystemTenancyBootstrapper::class, ], 'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/', - 'tenancy.filesystem.url_override.public' => 'storage-%tenant_id%' + 'tenancy.filesystem.url_override.public' => 'public-%tenant_id%' ]); $tenant1 = Tenant::create(); @@ -226,16 +229,57 @@ class BootstrapperTest extends TestCase tenancy()->initialize($tenant1); $this->assertEquals( - 'http://localhost/storage-'.$tenant1->getTenantKey().'/', + 'http://localhost/public-'.$tenant1->getTenantKey().'/', Storage::disk('public')->url('') ); tenancy()->initialize($tenant2); $this->assertEquals( - 'http://localhost/storage-'.$tenant2->getTenantKey().'/', + 'http://localhost/public-'.$tenant2->getTenantKey().'/', Storage::disk('public')->url('') ); } + /** @test */ + public function create_and_delete_storage_symlinks_jobs_works() + { + Event::listen( + TenantCreated::class, + JobPipeline::make([CreateStorageSymlinks::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener() + ); + + Event::listen( + TenantDeleted::class, + JobPipeline::make([RemoveStorageSymlinks::class])->send(function (TenantDeleted $event) { + return $event->tenant; + })->toListener() + ); + + config([ + 'tenancy.bootstrappers' => [ + FilesystemTenancyBootstrapper::class, + ], + 'tenancy.filesystem.suffix_base' => 'tenant-', + 'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/', + 'tenancy.filesystem.url_override.public' => 'public-%tenant_id%' + ]); + + /** @var \Stancl\Tenancy\Database\Models\Tenant $tenant */ + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + $tenant_key = $tenant->getTenantKey(); + + $this->assertDirectoryExists(storage_path("app/public")); + $this->assertEquals(storage_path("app/public/"), readlink(public_path("public-$tenant_key"))); + + $tenant->delete(); + + $this->assertDirectoryDoesNotExist(public_path("public-$tenant_key")); + } + // for queues see QueueTest } diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index ac6beb91..cf559281 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -34,7 +34,7 @@ class CommandsTest extends TestCase ], 'tenancy.filesystem.suffix_base' => 'tenant-', 'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/', - 'tenancy.filesystem.url_override.public' => 'storage-%tenant_id%' + 'tenancy.filesystem.url_override.public' => 'public-%tenant_id%' ]); Event::listen(TenancyInitialized::class, BootstrapTenancy::class); @@ -215,8 +215,35 @@ class CommandsTest extends TestCase Artisan::call('tenants:link'); $this->assertDirectoryExists(storage_path("tenant-$tenantId1/app/public")); - $this->assertDirectoryExists(public_path("storage-$tenantId1")); + $this->assertEquals(storage_path("tenant-$tenantId1/app/public/"), readlink(public_path("public-$tenantId1"))); + $this->assertDirectoryExists(storage_path("tenant-$tenantId2/app/public")); - $this->assertDirectoryExists(public_path("storage-$tenantId2")); + $this->assertEquals(storage_path("tenant-$tenantId2/app/public/"), readlink(public_path("public-$tenantId2"))); + + Artisan::call('tenants:link', [ + '--remove' => true, + ]); + + $this->assertDirectoryDoesNotExist(public_path("public-$tenantId1")); + $this->assertDirectoryDoesNotExist(public_path("public-$tenantId2")); + } + + /** @test */ + public function link_command_with_tenant_specified_works() + { + $tenant_key = Tenant::create()->getTenantKey(); + Artisan::call('tenants:link', [ + '--tenants' => [$tenant_key], + ]); + + $this->assertDirectoryExists(storage_path("tenant-$tenant_key/app/public")); + $this->assertEquals(storage_path("tenant-$tenant_key/app/public/"), readlink(public_path("public-$tenant_key"))); + + Artisan::call('tenants:link', [ + '--remove' => true, + '--tenants' => [$tenant_key], + ]); + + $this->assertDirectoryDoesNotExist(public_path("public-$tenant_key")); } }