diff --git a/README.md b/README.md index c493bc0..f80ba8a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,27 @@ This is a BIIGLE module that offers private storage disks for users. This module supports `s3`, `webdav`, `elements` and `aruna` storage disks but by default only S3 is enabled. Configure the enabled storage disk types as a comma-separated list with the `USER_DISKS_TYPES` environment variable (e.g. `s3,webdav`). +### Required Configuration by Disk Type + +Different storage disk types require additional packages to be installed: + +- **S3**: No additional packages required (included by default) +- **Aruna**: No additional packages required (included by default but disabled) +- **Elements**: Requires `biigle/laravel-elements-storage` + ```bash + composer require biigle/laravel-elements-storage + ``` +- **WebDAV**: Requires `biigle/laravel-webdav` + ```bash + composer require biigle/laravel-webdav + ``` +- **Azure**: Requires `biigle/laravel-azure-storage` + ```bash + composer require biigle/laravel-azure-storage + ``` + +Install only the packages for the disk types you plan to enable. + ## Installation 1. Run `composer require biigle/user-disks`. diff --git a/composer.json b/composer.json index aefeb02..820ad25 100644 --- a/composer.json +++ b/composer.json @@ -1,29 +1,35 @@ { "name": "biigle/user-disks", "description": "BIIGLE module to offer private storage disks for users.", - "keywords": ["biigle", "biigle-module"], + "keywords": [ + "biigle", + "biigle-module" + ], "license": "GPL-3.0-only", "support": { - "source": "https://github.com/biigle/user-disks", - "issues": "https://github.com/biigle/user-disks/issues" + "source": "https://github.com/biigle/user-disks", + "issues": "https://github.com/biigle/user-disks/issues" }, "homepage": "https://biigle.de", "authors": [ - { - "name": "Martin Zurowietz", - "email": "m.zurowietz@uni-bielefeld.de" - } + { + "name": "Martin Zurowietz", + "email": "m.zurowietz@uni-bielefeld.de" + } ], "require": { - "biigle/laravel-elements-storage": "^2.2", "league/flysystem-aws-s3-v3": "^3.12", - "league/flysystem-read-only": "^3.3", - "biigle/laravel-webdav": "^1.0" + "league/flysystem-read-only": "^3.3" + }, + "suggest": { + "biigle/laravel-elements-storage": "Required if the 'elements' disk type should be enabled", + "biigle/laravel-webdav": "Required if the 'webdav' disk type should be enabled", + "biigle/laravel-azure-storage": "Required if the 'azure' disk type should be enabled" }, "autoload": { - "psr-4": { - "Biigle\\Modules\\UserDisks\\": "src" - } + "psr-4": { + "Biigle\\Modules\\UserDisks\\": "src" + } }, "extra": { "laravel": { diff --git a/src/UserDisk.php b/src/UserDisk.php index 64d7143..17c5434 100644 --- a/src/UserDisk.php +++ b/src/UserDisk.php @@ -19,6 +19,7 @@ class UserDisk extends Model 'webdav' => 'WebDAV', 'elements' => 'Elements', 'aruna' => 'Aruna', + 'azure' => 'Azure Blob Storage', ]; /** diff --git a/src/config/user_disks.php b/src/config/user_disks.php index 969748b..914c613 100644 --- a/src/config/user_disks.php +++ b/src/config/user_disks.php @@ -2,7 +2,8 @@ return [ /* - | Available types for new storage disks. Supported are: s3, webdav, elements, aruna. + | Available types for new storage disks. Supported are: s3, webdav, elements, aruna, + | azure. */ 'types' => array_filter(explode(',', env('USER_DISKS_TYPES', 's3'))), @@ -62,6 +63,13 @@ 'secret' => '', 'endpoint' => '', ], + + 'azure' => [ + 'driver' => 'azure-storage-blob', + 'connection_string' => '', + 'container' => '', + 'use_direct_public_url' => true, + ], ], /* @@ -95,6 +103,11 @@ 'key' => 'required', 'secret' => 'required', ], + + 'azure' => [ + 'connection_string' => 'required', + 'container' => 'required', + ], ], /* @@ -128,6 +141,11 @@ 'key' => 'filled', 'secret' => 'filled', ], + + 'azure' => [ + 'connection_string' => 'filled', + 'container' => 'filled', + ], ], /* diff --git a/src/resources/views/manual/tutorials/about.blade.php b/src/resources/views/manual/tutorials/about.blade.php index 67085f8..a7fe8a4 100644 --- a/src/resources/views/manual/tutorials/about.blade.php +++ b/src/resources/views/manual/tutorials/about.blade.php @@ -65,6 +65,11 @@ Aruna @endif + @if(in_array('azure', config('user_disks.types'))) +
  • + Azure Blob Storage +
  • + @endif @if(empty(config('user_disks.types')))
  • No types are available. Please ask your administrator for help. @@ -87,5 +92,9 @@ @if(in_array('aruna', config('user_disks.types'))) @include("user-disks::manual.types.aruna") @endif + + @if(in_array('azure', config('user_disks.types'))) + @include("user-disks::manual.types.azure") + @endif @endsection diff --git a/src/resources/views/manual/types/azure.blade.php b/src/resources/views/manual/types/azure.blade.php new file mode 100644 index 0000000..b29c19b --- /dev/null +++ b/src/resources/views/manual/types/azure.blade.php @@ -0,0 +1,38 @@ +

    Azure Blob Storage

    + +

    + Azure Blob Storage is Microsoft's object storage solution for the cloud. An Azure storage disk can connect to one storage container in Azure. +

    + +

    + An Azure Blob Storage disk has the following options: +

    + +
    +
    SAS URL (optional)
    +
    +

    + If you provide a SAS URL, BIIGLE will auto-fill the connection string and container options (see below). Alternatively, you can set these options directly. +

    +
    +
    Connection String
    +
    +

    + The Azure Storage connection string. You can find this in the Azure Portal under your Storage Account → Access keys. +

    +

    + BIIGLE uses direct URLs to load files more efficiently in the browser. If the account name and key are provided in the connection string, BIIGLE can generate temporary URLs for this purpose. Otherwise it can use a signature from a SAS as direct URL. However, you will have to manually update the SAS in the storage disk configuration whenever it expires. +

    +

    + Example: +

    DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=...;EndpointSuffix=core.windows.net
    +

    +
    + +
    Container
    +
    +

    + The name of the container where your files are stored. +

    +
    +
    diff --git a/src/resources/views/store/azure.blade.php b/src/resources/views/store/azure.blade.php new file mode 100644 index 0000000..91384e9 --- /dev/null +++ b/src/resources/views/store/azure.blade.php @@ -0,0 +1,109 @@ +
    +
    +
    + + +

    + Paste your full SAS URL here to auto-fill the connection string and container fields below. +

    + @error('sas_url') +

    {{$message}}

    + @enderror +
    +
    + +
    +
    + + + @error('connection_string') +

    {{$message}}

    + @enderror +
    +
    + +
    +
    + + + @error('container') +

    {{$message}}

    + @enderror +
    +
    +
    + +@push('scripts') + + +@endpush diff --git a/src/resources/views/update/azure.blade.php b/src/resources/views/update/azure.blade.php new file mode 100644 index 0000000..f0937f5 --- /dev/null +++ b/src/resources/views/update/azure.blade.php @@ -0,0 +1,113 @@ +
    +
    +
    + + +

    + Paste your full SAS URL here to auto-fill the connection string and container fields below. +

    + @error('sas_url') +

    {{$message}}

    + @enderror +
    +
    + +
    +
    + + +

    + Leave empty to keep the current value. +

    + @error('connection_string') +

    {{$message}}

    + @enderror +
    +
    + +
    +
    + + + @error('container') +

    {{$message}}

    + @enderror +
    +
    +
    + +@push('scripts') + + +@endpush + diff --git a/tests/Http/Controllers/Api/UserDiskControllerTest.php b/tests/Http/Controllers/Api/UserDiskControllerTest.php index c2c89af..c5c68d8 100644 --- a/tests/Http/Controllers/Api/UserDiskControllerTest.php +++ b/tests/Http/Controllers/Api/UserDiskControllerTest.php @@ -695,6 +695,49 @@ public function testStoreAruna() $this->assertEquals($expect, $disk->options); } + public function testStoreAzure() + { + $this->beUser(); + + $this->mockController->shouldReceive('validateDiskAccess')->never(); + $this->postJson("/api/v1/user-disks", [ + 'name' => 'my disk', + 'type' => 'azure', + ]) + ->assertStatus(422) + ->assertJsonValidationErrors(['type']); + + config(['user_disks.types' => ['azure']]); + + $this->mockController->shouldReceive('validateDiskAccess')->never(); + $this->postJson("/api/v1/user-disks", [ + 'name' => 'my disk', + 'type' => 'azure', + ]) + ->assertStatus(422) + ->assertJsonValidationErrors(['connection_string', 'container']); + + $this->mockController->shouldReceive('validateDiskAccess')->once(); + $this->postJson("/api/v1/user-disks", [ + 'name' => 'my disk', + 'type' => 'azure', + 'connection_string' => 'DefaultEndpointsProtocol=https;BlobEndpoint=https://example.blob.core.windows.net;SharedAccessSignature=sv=...', + 'container' => 'example-container', + ]) + ->assertStatus(201); + + $disk = UserDisk::where('user_id', $this->user()->id)->first(); + $this->assertNotNull($disk); + $this->assertEquals('my disk', $disk->name); + $this->assertEquals('azure', $disk->type); + $this->assertNotNull($disk->expires_at); + $expect = [ + 'connection_string' => 'DefaultEndpointsProtocol=https;BlobEndpoint=https://example.blob.core.windows.net;SharedAccessSignature=sv=...', + 'container' => 'example-container', + ]; + $this->assertEquals($expect, $disk->options); + } + public function testUpdate() { $disk = UserDisk::factory()->create([ @@ -1347,6 +1390,38 @@ public function testUpdateAruna() $this->assertEquals($expect, $disk->options); } + public function testUpdateAzure() + { + config(['user_disks.types' => ['azure']]); + + $disk = UserDisk::factory()->create([ + 'type' => 'azure', + 'name' => 'abc', + 'options' => [ + 'connection_string' => 'DefaultEndpointsProtocol=https;BlobEndpoint=https://example.blob.core.windows.net;SharedAccessSignature=sv=...', + 'container' => 'example-container', + ], + ]); + + $this->be($disk->user); + $this->mockController->shouldReceive('validateDiskAccess')->once(); + $this->putJson("/api/v1/user-disks/{$disk->id}", [ + 'name' => 'cba', + 'connection_string' => 'DefaultEndpointsProtocol=https;BlobEndpoint=https://updated.blob.core.windows.net;SharedAccessSignature=sv=...', + 'container' => 'updated-container', + ]) + ->assertStatus(200); + + $disk->refresh(); + $expect = [ + 'connection_string' => 'DefaultEndpointsProtocol=https;BlobEndpoint=https://updated.blob.core.windows.net;SharedAccessSignature=sv=...', + 'container' => 'updated-container', + ]; + $this->assertEquals('azure', $disk->type); + $this->assertEquals('cba', $disk->name); + $this->assertEquals($expect, $disk->options); + } + public function testExtend() { config(['user_disks.about_to_expire_weeks' => 4]); diff --git a/tests/Http/Controllers/Views/UserDiskControllerTest.php b/tests/Http/Controllers/Views/UserDiskControllerTest.php index 8dfc738..e083f5d 100644 --- a/tests/Http/Controllers/Views/UserDiskControllerTest.php +++ b/tests/Http/Controllers/Views/UserDiskControllerTest.php @@ -53,6 +53,12 @@ public function testCreateAruna() $this->get('storage-disks/create?type=aruna&name=abc')->assertStatus(200); } + public function testCreateAzure() + { + $this->beUser(); + $this->get('storage-disks/create?type=azure&name=abc')->assertStatus(200); + } + public function testCreateInvalid() { $this->beUser(); @@ -120,4 +126,17 @@ public function testUpdateAruna() $this->be($disk->user); $this->get("storage-disks/{$disk->id}")->assertStatus(200); } + + public function testUpdateAzure() + { + $disk = UserDisk::factory()->create([ + 'type' => 'azure', + 'options' => [ + 'connection_string' => 'DefaultEndpointsProtocol=https;BlobEndpoint=https://example.blob.core.windows.net;SharedAccessSignature=sv=...', + 'container' => 'example-container', + ], + ]); + $this->be($disk->user); + $this->get("storage-disks/{$disk->id}")->assertStatus(200); + } } diff --git a/tests/UserDiskTest.php b/tests/UserDiskTest.php index d60529e..2310ac8 100644 --- a/tests/UserDiskTest.php +++ b/tests/UserDiskTest.php @@ -125,6 +125,31 @@ public function testGetS3Config() $this->assertEquals($expect, $disk->getConfig()); } + public function testGetAzureConfig() + { + $disk = UserDisk::factory()->make([ + 'type' => 'azure', + 'options' => [ + 'name' => 'account-name', + 'key' => 'account-key', + 'container' => 'container-name', + 'connection_string' => 'DefaultEndpointsProtocol=https;BlobEndpoint=https://mytest.blob.core.windows.net;SharedAccessSignature=sv=2025-07-05&spr=https&st=2025-11-26T16%3A59%3A32Z&se=2026-11-27T16%3A59%3A00Z&sr=c&sp=rl&sig=123412431234%3D', + ], + ]); + + $expect = [ + 'driver' => 'azure-storage-blob', + 'name' => 'account-name', + 'key' => 'account-key', + 'container' => 'container-name', + 'read-only' => true, + 'use_direct_public_url' => true, + 'connection_string' => 'DefaultEndpointsProtocol=https;BlobEndpoint=https://mytest.blob.core.windows.net;SharedAccessSignature=sv=2025-07-05&spr=https&st=2025-11-26T16%3A59%3A32Z&se=2026-11-27T16%3A59%3A00Z&sr=c&sp=rl&sig=123412431234%3D', + ]; + + $this->assertEquals($expect, $disk->getConfig()); + } + public function testGetConfigTemplateDoesNotExist() { $this->expectException(\TypeError::class); @@ -148,6 +173,11 @@ public function testGetStoreValidationRulesS3() $this->assertNotEmpty(UserDisk::getStoreValidationRules('s3')); } + public function testGetStoreValidationRulesAzure() + { + $this->assertNotEmpty(UserDisk::getStoreValidationRules('azure')); + } + public function testGetUpdateValidationRules() { $rules = [ @@ -164,6 +194,11 @@ public function testGetUpdateValidationRulesS3() $this->assertNotEmpty(UserDisk::getUpdateValidationRules('s3')); } + public function testGetUpdateValidationRulesAzure() + { + $this->assertNotEmpty(UserDisk::getUpdateValidationRules('azure')); + } + public function testIsAboutToExpire() { config(['user_disks.about_to_expire_weeks' => 4]);