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 @@
+
+
+@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 @@
+
+
+@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]);