Skip to content

Commit

Permalink
Flattening work by @neildsouth to add Azure Blob Storage support
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Woodhead <[email protected]>
  • Loading branch information
woodheadio committed Aug 1, 2024
1 parent 1757165 commit b1b2a76
Show file tree
Hide file tree
Showing 23 changed files with 1,434 additions and 0 deletions.
14 changes: 14 additions & 0 deletions src/Monai.Deploy.Storage.sln
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.Storage.MinIO"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.Storage.MinIO.Tests", "Plugins\MinIO\Tests\Monai.Deploy.Storage.MinIO.Tests.csproj", "{FCB3FCA4-2BB6-4921-9715-CDECF343C6E2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monai.Deploy.Storage.AzureBlob", "Plugins\AzureBlob\Monai.Deploy.Storage.AzureBlob\Monai.Deploy.Storage.AzureBlob.csproj", "{054DC580-A3ED-48AE-9706-E0CFDE6C171F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monai.Deploy.Storage.AzureBlob.Tests", "Plugins\AzureBlob\Monai.Deploy.Storage.AzureBlob.Tests\Monai.Deploy.Storage.AzureBlob.Tests.csproj", "{FDADE27C-70DC-401D-B058-041D4A587548}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -57,6 +61,14 @@ Global
{FCB3FCA4-2BB6-4921-9715-CDECF343C6E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCB3FCA4-2BB6-4921-9715-CDECF343C6E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCB3FCA4-2BB6-4921-9715-CDECF343C6E2}.Release|Any CPU.Build.0 = Release|Any CPU
{054DC580-A3ED-48AE-9706-E0CFDE6C171F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{054DC580-A3ED-48AE-9706-E0CFDE6C171F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{054DC580-A3ED-48AE-9706-E0CFDE6C171F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{054DC580-A3ED-48AE-9706-E0CFDE6C171F}.Release|Any CPU.Build.0 = Release|Any CPU
{FDADE27C-70DC-401D-B058-041D4A587548}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FDADE27C-70DC-401D-B058-041D4A587548}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FDADE27C-70DC-401D-B058-041D4A587548}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FDADE27C-70DC-401D-B058-041D4A587548}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -65,6 +77,8 @@ Global
{9E605292-D0F4-4E56-B723-D98397E07A77} = {0F380AAC-016C-4B0E-808E-059F6F17EB29}
{0292D249-4FDD-4C4A-9D81-669E4375D23A} = {0F380AAC-016C-4B0E-808E-059F6F17EB29}
{FCB3FCA4-2BB6-4921-9715-CDECF343C6E2} = {0F380AAC-016C-4B0E-808E-059F6F17EB29}
{054DC580-A3ED-48AE-9706-E0CFDE6C171F} = {0F380AAC-016C-4B0E-808E-059F6F17EB29}
{FDADE27C-70DC-401D-B058-041D4A587548} = {0F380AAC-016C-4B0E-808E-059F6F17EB29}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E1105263-9CBF-45AA-BAC3-BD8504C1B962}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2022 MONAI Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using Xunit;
//Optional
[assembly: CollectionBehavior(DisableTestParallelization = true)]
//Optional
[assembly: TestCaseOrderer("Xunit.Extensions.Ordering.TestCaseOrderer", "Xunit.Extensions.Ordering")]
//Optional
[assembly: TestCollectionOrderer("Xunit.Extensions.Ordering.CollectionOrderer", "Xunit.Extensions.Ordering")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Monai.Deploy.Storage.AzureBlob.Tests
{
public class Class1
{

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
using Xunit.Extensions.Ordering;

namespace Monai.Deploy.Storage.AzureBlob.Tests.Integration
{
[Order(0), Collection("AzureBlobStorage")]
public class AzureBlobServiceTests
{
private readonly Mock<ILogger<AzureBlobStorageService>> _logger;
private readonly AzureBlobStorageFixture _fixture;
private readonly AzureBlobStorageService _azureBlobService;
private readonly string _testFileName;
private readonly string _testFileNameCopy;

public AzureBlobServiceTests(AzureBlobStorageFixture fixture)
{
_logger = new Mock<ILogger<AzureBlobStorageService>>();
_fixture = fixture ?? throw new ArgumentNullException(nameof(fixture));
_azureBlobService = new AzureBlobStorageService(_fixture.ClientFactory, _fixture.Configurations, _logger.Object);
_testFileName = $"Tao-Te-Ching/Laozi/chapter-one.zip";
_testFileNameCopy = $"Tao-Te-Ching/Laozi/chapter-one=backup.zip";
}


[Fact, Order(1)]
public async Task S01_GivenABucketToAzureBlob()
{
var exception = await Record.ExceptionAsync(async () =>
{
//await _azureBlobService.CreateFolderAsync(_fixture.ContainerName, "");
var containerClient = _fixture.ClientFactory.GetBlobContainerClient(_fixture.ContainerName);
await containerClient.CreateIfNotExistsAsync().ConfigureAwait(false);
}).ConfigureAwait(false);

Assert.Null(exception);
}

[Fact, Order(2)]
public async Task S02_GivenASetOfDataAvailableToAzureBlob()
{
var exception = await Record.ExceptionAsync(async () =>
{
await _fixture.GenerateAndUploadData().ConfigureAwait(false);
}).ConfigureAwait(false);

Assert.Null(exception);
}

[Theory, Order(3)]
[InlineData(null, 4)]
[InlineData("dir-1/", 1)]
[InlineData("dir-2/", 2)]
public async Task S03_WhenListObjectsAsyncIsCalled_ExpectItToListObjectsBasedOnParameters(string? prefix, int count)
{
var actual = await _azureBlobService.ListObjectsAsync(_fixture.ContainerName, prefix, true).ConfigureAwait(false);

actual.Should().NotBeEmpty()
.And.HaveCount(count);

var expected = _fixture.Files.ToList();
if (prefix is not null)
{
expected = expected.Where(p => p.StartsWith(prefix)).ToList();
}
actual.Select(p => p.FilePath).Should().BeEquivalentTo(expected);
}

[Fact, Order(4)]
public async Task S04_WhenVerifyObjectsExistAsyncIsCalled_ExpectToReturnAll()
{
var actual = await _azureBlobService.VerifyObjectsExistAsync(_fixture.ContainerName, _fixture.Files).ConfigureAwait(false);

actual.Should().NotBeEmpty()
.And.HaveCount(_fixture.Files.Count);

actual.Should().ContainValues(true);
}

[Fact, Order(5)]
public async Task S05_GivenAFileUploadedToAzureBlob()
{
var data = _fixture.GetRandomBytes();
var stream = new MemoryStream(data);
await _azureBlobService.PutObjectAsync(_fixture.ContainerName, _testFileName, stream, data.Length, "application/binary", null).ConfigureAwait(false);

var callback = (Stream stream) =>
{
var actual = new MemoryStream();
stream.CopyTo(actual);
actual.ToArray().Should().Equal(data);
};
var client = _fixture.ClientFactory.GetBlobClient(_fixture.ContainerName, _testFileName);

var fileContentsStream = new MemoryStream();
await client.DownloadToAsync(fileContentsStream).ConfigureAwait(false);
fileContentsStream.ToArray().Should().Equal(data);

var prop = await client.GetPropertiesAsync().ConfigureAwait(false);
prop.Value.ContentLength.Should().Be(data.Length);
}

[Fact, Order(6)]
public async Task S06_ExpectTheFileToBeBeDownloadable()
{
var stream = await _azureBlobService.GetObjectAsync(_fixture.ContainerName, _testFileName).ConfigureAwait(false);
Assert.NotNull(stream);
var ms = new MemoryStream();
stream.CopyTo(ms);
var data = ms.ToArray();

var original = await DownloadData(_testFileName).ConfigureAwait(false);

Assert.NotNull(original);

data.Should().Equal(original);
}

[Fact, Order(7)]
public async Task S07_GivenACopyOfTheFile()
{
await _azureBlobService.CopyObjectAsync(_fixture.ContainerName, _testFileName, _fixture.ContainerName, _testFileNameCopy).ConfigureAwait(false);

var original = await DownloadData(_testFileName).ConfigureAwait(false);
var copy = await DownloadData(_testFileNameCopy).ConfigureAwait(false);

Assert.NotNull(original);
Assert.NotNull(copy);

copy.Should().Equal(original);
}

[Fact, Order(8)]
public async Task S08_ExpectedBothOriginalAndCopiedToExist()
{
var files = new List<string>() { _testFileName, _testFileNameCopy, "file-does-not-exist" };
var expectedResults = new List<bool>() { true, true, false };
var results = await _azureBlobService.VerifyObjectsExistAsync(_fixture.ContainerName, files).ConfigureAwait(false);

Assert.NotNull(results);

results.Should().ContainKeys(files);
results.Should().ContainValues(expectedResults);

for (var i = 0; i < files.Count; i++)
{
var file = files[i];
var result = await _azureBlobService.VerifyObjectExistsAsync(_fixture.ContainerName, file).ConfigureAwait(false);
Assert.Equal(expectedResults[i], result);
}
}

[Fact, Order(9)]
public async Task S09_GivenADirectoryCreatedToAzureBlob()
{
var folderName = "my-folder";
await _azureBlobService.CreateFolderAsync(_fixture.ContainerName, folderName).ConfigureAwait(false);
var result = await _azureBlobService.VerifyObjectExistsAsync(_fixture.ContainerName, $"{folderName}/stubFile.txt").ConfigureAwait(false);

Assert.True(result);
}

[Fact, Order(10)]
public async Task S10_ExpectTheDirectoryToBeRemovable()
{
var folderName = "my - folder / stubFile.txt";
await _azureBlobService.RemoveObjectAsync(_fixture.ContainerName, folderName).ConfigureAwait(false);
var result = await _azureBlobService.VerifyObjectExistsAsync(_fixture.ContainerName, $"{folderName}/stubFile.txt").ConfigureAwait(false);
Assert.False(result);

var files = new List<string>() { _testFileName, _testFileNameCopy, "file-does-not-exist" };
await _azureBlobService.RemoveObjectsAsync(_fixture.ContainerName, files).ConfigureAwait(false);
}

[Fact, Order(11)]
public async Task S11_ExpectTheFilesToBeRemovable()
{
var files = new List<string>() { _testFileName, _testFileNameCopy, "file-does-not-exist" };
await _azureBlobService.RemoveObjectsAsync(_fixture.ContainerName, files).ConfigureAwait(false);

for (var i = 0; i < files.Count; i++)
{
var file = files[i];
var result = await _azureBlobService.VerifyObjectExistsAsync(_fixture.ContainerName, file).ConfigureAwait(false);
Assert.False(result);
}
}

private async Task<byte[]> DownloadData(string filename)
{
var copiedStream = new MemoryStream();

var client = _fixture.ClientFactory.GetBlobClient(_fixture.ContainerName, filename);
await client.DownloadToAsync(copiedStream).ConfigureAwait(false);
return copiedStream.ToArray();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright 2022 MONAI Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Monai.Deploy.Storage.Configuration;
using Moq;
using Xunit;

namespace Monai.Deploy.Storage.AzureBlob.Tests.Integration
{
[CollectionDefinition("AzureBlobStorage")]
public class AzureBlobStorageCollection : ICollectionFixture<AzureBlobStorageFixture>
{
// This class has no code, and is never created. Its purpose is simply
// to be the place to apply [CollectionDefinition] and all the
// ICollectionFixture<> interfaces.
}

public class AzureBlobStorageFixture : IAsyncDisposable
{
const int MaxFileSize = 104857600;
private readonly Random _random;
private readonly List<string> _files;

public IOptions<StorageServiceConfiguration> Configurations { get; }
public AzureBlobClientFactory ClientFactory { get; }
public IReadOnlyList<string> Files { get => _files; }
public string ContainerName { get; }

public AzureBlobStorageFixture()
{
_random = new Random();
_files = new List<string>();

Configurations = Options.Create(new StorageServiceConfiguration());
Configurations.Value.Settings.Add(ConfigurationKeys.ConnectionString, "UseDevelopmentStorage=true");

ClientFactory = new AzureBlobClientFactory(Configurations, new Mock<ILogger<AzureBlobClientFactory>>().Object);
ContainerName = $"md-test-{_random.Next(1000)}";
}

internal async Task GenerateAndUploadData()
{
await GenerateAndUploadFile($"{Guid.NewGuid()}").ConfigureAwait(false);
await GenerateAndUploadFile($"dir-1/{Guid.NewGuid()}").ConfigureAwait(false);
await GenerateAndUploadFile($"dir-2/{Guid.NewGuid()}").ConfigureAwait(false);
await GenerateAndUploadFile($"dir-2/a/b/{Guid.NewGuid()}").ConfigureAwait(false);
}

private async Task GenerateAndUploadFile(string filePath)
{
var data = GetRandomBytes();
var stream = new MemoryStream(data);
var client = ClientFactory.GetBlobClient(ContainerName, filePath);
await client.UploadAsync(stream).ConfigureAwait(false);
_files.Add(filePath);
}

public byte[] GetRandomBytes()
{
return new byte[_random.Next(1, MaxFileSize)];
}

public async ValueTask DisposeAsync()
{
await RemoveData().ConfigureAwait(false);
await RemoveBucket().ConfigureAwait(false);
}

private async Task RemoveBucket()
{
var client = ClientFactory.GetBlobContainerClient(ContainerName);
var exists = await client.ExistsAsync().ConfigureAwait(false);
if (exists)
{
var resultSegment = client.GetBlobsAsync(prefix: ContainerName).AsPages(default, 100);

await foreach (var blobPage in resultSegment)
{
foreach (var blobItem in blobPage.Values)
{
await ClientFactory.GetBlobClient(ContainerName, blobItem.Name).DeleteAsync().ConfigureAwait(false);
};
}
}
}

private async Task RemoveData()
{
await RemoveBucket().ConfigureAwait(false);
}
}
}
Loading

0 comments on commit b1b2a76

Please sign in to comment.