diff --git a/src/Controllers/TenantAssetController.php b/src/Controllers/TenantAssetController.php index bd27e7be..1863c975 100644 --- a/src/Controllers/TenantAssetController.php +++ b/src/Controllers/TenantAssetController.php @@ -29,6 +29,13 @@ class TenantAssetController implements HasMiddleware */ public static array $middleware = []; + /** + * Disk the assets are served from. + * + * When null, the assets are served from app/public inside the tenant's storage directory. + */ + public static string|null $publicDisk = null; + public static function middleware() { return array_map( @@ -59,13 +66,25 @@ class TenantAssetController implements HasMiddleware } /** - * Assets are served from app/public inside the tenant's storage directory. + * Directory the assets are served from -- the root of the $publicDisk, + * or app/public inside the tenant's storage directory when no disk is configured. * - * The directory is resolved using the FilesystemTenancyBootstrapper (rather than storage_path(), - * so that it's tenant-scoped regardless of the suffix_storage_path config). + * The storage directory is resolved using the FilesystemTenancyBootstrapper (rather than + * storage_path(), so that it's tenant-scoped regardless of the suffix_storage_path config). */ protected function assetRoot(): string { + if (static::$publicDisk) { + $diskRoot = config('filesystems.disks.' . static::$publicDisk . '.root'); + + if (! is_string($diskRoot)) { + // A disk with no root path would let the controller serve any file in the app + throw new Exception('Disk [' . static::$publicDisk . '] has no root path configured.'); + } + + return rtrim($diskRoot, '/'); + } + if ($tenant = tenant()) { return FilesystemTenancyBootstrapper::getBoundTenantStoragePath($tenant) . '/app/public'; } diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index 4e6e550b..7f1b9ab0 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -30,6 +30,7 @@ beforeEach(function () { TenancyUrlGenerator::$prefixRouteNames = false; TenancyUrlGenerator::$passTenantParameterToRoutes = true; TenantAssetController::$headers = []; + TenantAssetController::$publicDisk = null; /** @var CloneRoutesAsTenant $cloneAction */ $cloneAction = app(CloneRoutesAsTenant::class); @@ -88,6 +89,47 @@ test('tenant assets are served even when the suffix_storage_path config is set t ->toBe("$centralStoragePath/tenant{$tenant->id}/app/public/$filename"); }); +test('the disk used for serving tenant assets is configurable', function () { + config([ + 'tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class, + // This is tenancy's default override for the local disk's root, set it here for clarity + 'tenancy.filesystem.root_override.local' => '%storage_path%/app/', + ]); + + // The local disk's root is overridden to '%storage_path%/app/' (so it does not use 'app/public') + TenantAssetController::$publicDisk = 'local'; + + $tenant = Tenant::create(); + tenancy()->initialize($tenant); + + $filename = 'testfile' . Str::random(8); + Storage::disk('local')->put($filename, 'bar'); + $path = Storage::disk('local')->path($filename); + + $response = pest()->get(tenant_asset($filename), ['X-Tenant' => $tenant->id]); + + // The asset is served from the disk's root instead of 'app/public' + $response->assertSuccessful(); + expect($response->getFile()->getPathname())->toBe($path); +}); + +test('tenant asset controller throws when the configured disk has no root', function () { + config([ + 'tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class, + 'filesystems.disks.rootless' => ['driver' => 's3'], + ]); + + TenantAssetController::$publicDisk = 'rootless'; + + $tenant = Tenant::create(); + tenancy()->initialize($tenant); + + $this->withoutExceptionHandling(); + pest()->expectExceptionMessage('Disk [rootless] has no root path configured.'); + + pest()->get(tenant_asset('foo.txt'), ['X-Tenant' => $tenant->id]); +}); + test('asset helper returns a link to tenant asset controller when asset url is null', function () { config(['app.asset_url' => null]); config(['tenancy.filesystem.asset_helper_override' => true]);