diff --git a/composer.json b/composer.json index aefeb02..300d096 100644 --- a/composer.json +++ b/composer.json @@ -1,29 +1,33 @@ { "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" + "biigle/laravel-webdav": "^1.0", + "azure-oss/storage-blob-laravel": "^1.4" }, "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..67c0336 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,12 @@ 'secret' => '', 'endpoint' => '', ], + + 'azure' => [ + 'driver' => 'azure-storage-blob', + 'connection_string' => '', + 'container' => '', + ], ], /* @@ -95,6 +102,11 @@ 'key' => 'required', 'secret' => 'required', ], + + 'azure' => [ + 'connection_string' => 'required', + 'container' => 'required', + ], ], /* @@ -128,6 +140,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..efae308 --- /dev/null +++ b/src/resources/views/manual/types/azure.blade.php @@ -0,0 +1,39 @@ +

    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
    +
    +

    + 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. +

    +

    + Example: +

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

    +

    + For local development with Azurite, use: +

    DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;
    +

    +
    + +
    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..8eff677 --- /dev/null +++ b/src/resources/views/store/azure.blade.php @@ -0,0 +1,108 @@ +
    +
    +
    + + +

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

    + @error('sas_url') +

    {{$message}}

    + @enderror +
    +
    + +
    +
    + + +

    + Will be autofilled if SAS URL is given. You can find the connection string in the Azure Portal under your Storage Account → Security + networking → Access keys. +

    + @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..a5381a8 --- /dev/null +++ b/src/resources/views/update/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 +
    +
    + +
    +
    + + +

    + Will be autofilled if SAS URL is given. You can find the connection string in the Azure Portal under your Storage Account → Security + networking → Access keys. 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..2523fe7 100644 --- a/tests/UserDiskTest.php +++ b/tests/UserDiskTest.php @@ -125,6 +125,30 @@ 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, + '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 +172,11 @@ public function testGetStoreValidationRulesS3() $this->assertNotEmpty(UserDisk::getStoreValidationRules('s3')); } + public function testGetStoreValidationRulesAzure() + { + $this->assertNotEmpty(UserDisk::getStoreValidationRules('azure')); + } + public function testGetUpdateValidationRules() { $rules = [ @@ -164,6 +193,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]);