diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index eac13cf8..d4bd9956 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -14,75 +14,39 @@ jobs: build-and-test: runs-on: ubuntu-latest - services: - localstack: - image: localstack/localstack:latest - ports: - - 4563-4599:4563-4599 - - 8055:8080 - env: - SERVICES: s3 - DEBUG: 1 - DATA_DIR: /tmp/localstack/data - AWS_SECRET_KEY: 'localkey' - AWS_BUCKET_NAME: 'managed-code-bucket' - AWS_ACCESS_KEY_ID: 'localkey' - AWS_SECRET_ACCESS_KEY: 'localsecret' - DEFAULT_REGION: 'eu-west-1' steps: + + - name: checkout + uses: actions/checkout@v3 - - uses: actions/checkout@v3 - name: Setup .NET uses: actions/setup-dotnet@v3 with: dotnet-version: 7.0.x - - - name: NDepend - uses: ndepend/ndepend-action@v1 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - license: ${{ secrets.NDEPENDLICENSE }} - - - name: azuright - uses: potatoqualitee/azuright@v1.1 - - - name: docker run fake-gcs-server - run: | - docker run -d --name fake-gcs-server -p 4443:4443 -v ${PWD}/examples/data:/data fsouza/fake-gcs-server -scheme http -external-url "http://localhost:4443" - sleep 5s - - - name: check storage emulators - run: | - curl http://localhost:4443/ - curl http://localhost:4566/ - curl http://localhost:10000/ - - # run build and test + - name: Restore dependencies run: dotnet restore + - name: Build run: dotnet build --no-restore + - name: Test run: dotnet test --no-build --logger 'trx;LogFileName=test-results.trx' - env: - DEFAULT_REGION: eu-west-1 - AWS_ACCESS_KEY_ID: localkey - AWS_SECRET_ACCESS_KEY: localsecret + - name: Collect Code Coverage run: dotnet test --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=ManagedCode.Storage.Tests/lcov.info - env: - DEFAULT_REGION: eu-west-1 - AWS_ACCESS_KEY_ID: localkey - AWS_SECRET_ACCESS_KEY: localsecret + + + - name: NDepend + uses: ndepend/ndepend-action@v1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + license: ${{ secrets.NDEPENDLICENSE }} + coveragefolder: ManagedCode.Storage.Tests + baseline: recent + #baseline: main_recent -# - name: test-reports -# uses: dorny/test-reporter@v1.5.0 -# with: -# name: Test Reporter -# reporter: dotnet-trx -# path: ManagedCode.Storage.Tests/test-results.trx - - name : coverlet uses: b3b00/coverlet-action@1.1.9 with: @@ -95,3 +59,5 @@ jobs: with: github-token: ${{secrets.GITHUB_TOKEN }} path-to-lcov: ManagedCode.Storage.Tests/lcov.info + + diff --git a/Directory.Build.props b/Directory.Build.props index 84bb38d7..1ee2a624 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -17,8 +17,8 @@ https://github.com/managedcode/Storage https://github.com/managedcode/Storage Managed Code - Storage - 2.1.14 - 2.1.14 + 2.1.15-alpha + 2.1.15-alpha diff --git a/ManagedCode.Storage.Aws/AWSStorage.cs b/ManagedCode.Storage.Aws/AWSStorage.cs index 0eb00e17..5bec1579 100644 --- a/ManagedCode.Storage.Aws/AWSStorage.cs +++ b/ManagedCode.Storage.Aws/AWSStorage.cs @@ -34,7 +34,7 @@ public override async Task RemoveContainerAsync(CancellationToken cancel } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -104,7 +104,7 @@ protected override async Task CreateContainerInternalAsync(CancellationT } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -124,7 +124,7 @@ protected override async Task DeleteDirectoryInternalAsync(string direct } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -183,7 +183,7 @@ await localFile.CopyFromStreamAsync(await StorageClient.GetObjectStreamAsync(Sto } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -211,7 +211,7 @@ await StorageClient.DeleteObjectAsync(new DeleteObjectRequest } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -235,7 +235,7 @@ protected override async Task> ExistsInternalAsync(ExistOptions opt } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -269,7 +269,7 @@ protected override async Task> GetBlobMetadataInternalAsync } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -301,7 +301,7 @@ protected override async Task SetLegalHoldInternalAsync(bool hasLegalHol } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -323,7 +323,7 @@ protected override async Task> HasLegalHoldInternalAsync(LegalHoldO } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } diff --git a/ManagedCode.Storage.Aws/BlobStream.cs b/ManagedCode.Storage.Aws/BlobStream.cs new file mode 100644 index 00000000..3f38a01e --- /dev/null +++ b/ManagedCode.Storage.Aws/BlobStream.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Amazon.S3; +using Amazon.S3.Model; + +namespace ManagedCode.Storage.Aws; + +public class BlobStream : Stream + { + /* Note the that maximum size (as of now) of a file in S3 is 5TB so it isn't + * safe to assume all uploads will work here. MAX_PART_SIZE times MAX_PART_COUNT + * is ~50TB, which is too big for S3. */ + const long MIN_PART_LENGTH = 5L * 1024 * 1024; // all parts but the last this size or greater + const long MAX_PART_LENGTH = 5L * 1024 * 1024 * 1024; // 5GB max per PUT + const long MAX_PART_COUNT = 10000; // no more than 10,000 parts total + const long DEFAULT_PART_LENGTH = MIN_PART_LENGTH; + + internal class Metadata + { + public string BucketName; + public string Key; + public long PartLength = DEFAULT_PART_LENGTH; + + public int PartCount = 0; + public string UploadId; + public MemoryStream CurrentStream; + + public long Position = 0; // based on bytes written + public long Length = 0; // based on bytes written or SetLength, whichever is larger (no truncation) + + public List Tasks = new List(); + public ConcurrentDictionary PartETags = new ConcurrentDictionary(); + } + + Metadata _metadata = new Metadata(); + IAmazonS3 _s3 = null; + + public BlobStream(IAmazonS3 s3, string s3uri, long partLength = DEFAULT_PART_LENGTH) + : this(s3, new Uri(s3uri), partLength) + { + } + + public BlobStream(IAmazonS3 s3, Uri s3uri, long partLength = DEFAULT_PART_LENGTH) + : this (s3, s3uri.Host, s3uri.LocalPath.Substring(1), partLength) + { + } + + public BlobStream(IAmazonS3 s3, string bucket, string key, long partLength = DEFAULT_PART_LENGTH) + { + _s3 = s3; + _metadata.BucketName = bucket; + _metadata.Key = key; + _metadata.PartLength = partLength; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_metadata != null) + { + Flush(true); + CompleteUpload(); + } + } + _metadata = null; + base.Dispose(disposing); + } + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => _metadata.Length = Math.Max(_metadata.Length, _metadata.Position); + + public override long Position + { + get => _metadata.Position; + set => throw new NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); + + public override void SetLength(long value) + { + _metadata.Length = Math.Max(_metadata.Length, value); + _metadata.PartLength = Math.Max(MIN_PART_LENGTH, Math.Min(MAX_PART_LENGTH, _metadata.Length / MAX_PART_COUNT)); + } + + private void StartNewPart() + { + if (_metadata.CurrentStream != null) { + Flush(false); + } + _metadata.CurrentStream = new MemoryStream(); + _metadata.PartLength = Math.Min(MAX_PART_LENGTH, Math.Max(_metadata.PartLength, (_metadata.PartCount / 2 + 1) * MIN_PART_LENGTH)); + } + + public override void Flush() + { + Flush(false); + } + + private void Flush(bool disposing) + { + if ((_metadata.CurrentStream == null || _metadata.CurrentStream.Length < MIN_PART_LENGTH) && + !disposing) + return; + + if (_metadata.UploadId == null) { + _metadata.UploadId = _s3.InitiateMultipartUploadAsync(new InitiateMultipartUploadRequest() + { + BucketName = _metadata.BucketName, + Key = _metadata.Key + }).GetAwaiter().GetResult().UploadId; + } + + if (_metadata.CurrentStream != null) + { + var i = ++_metadata.PartCount; + + _metadata.CurrentStream.Seek(0, SeekOrigin.Begin); + var request = new UploadPartRequest() + { + BucketName = _metadata.BucketName, + Key = _metadata.Key, + UploadId = _metadata.UploadId, + PartNumber = i, + IsLastPart = disposing, + InputStream = _metadata.CurrentStream + }; + _metadata.CurrentStream = null; + + var upload = Task.Run(async () => + { + var response = await _s3.UploadPartAsync(request); + _metadata.PartETags.AddOrUpdate(i, response.ETag, + (n, s) => response.ETag); + request.InputStream.Dispose(); + }); + _metadata.Tasks.Add(upload); + } + } + + private void CompleteUpload() + { + Task.WaitAll(_metadata.Tasks.ToArray()); + + if (Length > 0) { + _s3.CompleteMultipartUploadAsync(new CompleteMultipartUploadRequest() + { + BucketName = _metadata.BucketName, + Key = _metadata.Key, + PartETags = _metadata.PartETags.Select(e => new PartETag(e.Key, e.Value)).ToList(), + UploadId = _metadata.UploadId + }).GetAwaiter().GetResult(); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (count == 0) return; + + // write as much of the buffer as will fit to the current part, and if needed + // allocate a new part and continue writing to it (and so on). + var o = offset; + var c = Math.Min(count, buffer.Length - offset); // don't over-read the buffer, even if asked to + do + { + if (_metadata.CurrentStream == null || _metadata.CurrentStream.Length >= _metadata.PartLength) + StartNewPart(); + + var remaining = _metadata.PartLength - _metadata.CurrentStream.Length; + var w = Math.Min(c, (int)remaining); + _metadata.CurrentStream.Write(buffer, o, w); + + _metadata.Position += w; + c -= w; + o += w; + } while (c > 0); + } + } diff --git a/ManagedCode.Storage.Aws/ManagedCode.Storage.Aws.csproj b/ManagedCode.Storage.Aws/ManagedCode.Storage.Aws.csproj index 62117abf..00b41d63 100644 --- a/ManagedCode.Storage.Aws/ManagedCode.Storage.Aws.csproj +++ b/ManagedCode.Storage.Aws/ManagedCode.Storage.Aws.csproj @@ -22,10 +22,10 @@ - - + + - + diff --git a/ManagedCode.Storage.Aws/Options/AWSStorageOptions.cs b/ManagedCode.Storage.Aws/Options/AWSStorageOptions.cs index 11cc2f80..a3e21081 100644 --- a/ManagedCode.Storage.Aws/Options/AWSStorageOptions.cs +++ b/ManagedCode.Storage.Aws/Options/AWSStorageOptions.cs @@ -3,10 +3,12 @@ namespace ManagedCode.Storage.Aws.Options; -public class AWSStorageOptions : StorageOptions +public class AWSStorageOptions : IStorageOptions { public string? PublicKey { get; set; } public string? SecretKey { get; set; } public string? Bucket { get; set; } public AmazonS3Config? OriginalOptions { get; set; } = new(); + + public bool CreateContainerIfNotExists { get; set; } = true; } \ No newline at end of file diff --git a/ManagedCode.Storage.AzureDataLake/AzureDataLakeStorage.cs b/ManagedCode.Storage.Azure.DataLake/AzureDataLakeStorage.cs similarity index 94% rename from ManagedCode.Storage.AzureDataLake/AzureDataLakeStorage.cs rename to ManagedCode.Storage.Azure.DataLake/AzureDataLakeStorage.cs index fd63b3b3..1fe4e227 100644 --- a/ManagedCode.Storage.AzureDataLake/AzureDataLakeStorage.cs +++ b/ManagedCode.Storage.Azure.DataLake/AzureDataLakeStorage.cs @@ -9,12 +9,12 @@ using Azure.Storage.Files.DataLake.Models; using ManagedCode.Communication; using ManagedCode.Communication.Extensions; -using ManagedCode.Storage.AzureDataLake.Options; +using ManagedCode.Storage.Azure.DataLake.Options; using ManagedCode.Storage.Core; using ManagedCode.Storage.Core.Models; using Microsoft.Extensions.Logging; -namespace ManagedCode.Storage.AzureDataLake; +namespace ManagedCode.Storage.Azure.DataLake; public class AzureDataLakeStorage : BaseStorage, IAzureDataLakeStorage { @@ -38,7 +38,7 @@ public override async Task RemoveContainerAsync(CancellationToken cancel } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -54,7 +54,7 @@ public async Task> OpenReadStreamAsync(OpenReadStreamOptions opti } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -70,7 +70,7 @@ public async Task> OpenWriteStreamAsync(OpenWriteStreamOptions op } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -130,7 +130,7 @@ await _dataLakeServiceClient.CreateFileSystemAsync(StorageOptions.FileSystem, St } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -156,7 +156,7 @@ protected override async Task> UploadInternalAsync(Stream s } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -187,7 +187,7 @@ protected override async Task> DownloadInternalAsync(LocalFile } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -202,7 +202,7 @@ protected override async Task> DeleteInternalAsync(DeleteOptions op } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -217,7 +217,7 @@ protected override async Task> ExistsInternalAsync(ExistOptions opt } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -246,7 +246,7 @@ protected override async Task> GetBlobMetadataInternalAsync } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } diff --git a/ManagedCode.Storage.AzureDataLake/Extensions/ServiceCollectionExtensions.cs b/ManagedCode.Storage.Azure.DataLake/Extensions/ServiceCollectionExtensions.cs similarity index 95% rename from ManagedCode.Storage.AzureDataLake/Extensions/ServiceCollectionExtensions.cs rename to ManagedCode.Storage.Azure.DataLake/Extensions/ServiceCollectionExtensions.cs index d2051ff4..de832309 100644 --- a/ManagedCode.Storage.AzureDataLake/Extensions/ServiceCollectionExtensions.cs +++ b/ManagedCode.Storage.Azure.DataLake/Extensions/ServiceCollectionExtensions.cs @@ -1,10 +1,10 @@ using System; -using ManagedCode.Storage.AzureDataLake.Options; +using ManagedCode.Storage.Azure.DataLake.Options; using ManagedCode.Storage.Core; using ManagedCode.Storage.Core.Exceptions; using Microsoft.Extensions.DependencyInjection; -namespace ManagedCode.Storage.AzureDataLake.Extensions; +namespace ManagedCode.Storage.Azure.DataLake.Extensions; public static class ServiceCollectionExtensions { diff --git a/ManagedCode.Storage.AzureDataLake/IAzureDataLakeStorage.cs b/ManagedCode.Storage.Azure.DataLake/IAzureDataLakeStorage.cs similarity index 90% rename from ManagedCode.Storage.AzureDataLake/IAzureDataLakeStorage.cs rename to ManagedCode.Storage.Azure.DataLake/IAzureDataLakeStorage.cs index 14c6e267..25de14b4 100644 --- a/ManagedCode.Storage.AzureDataLake/IAzureDataLakeStorage.cs +++ b/ManagedCode.Storage.Azure.DataLake/IAzureDataLakeStorage.cs @@ -3,10 +3,10 @@ using System.Threading.Tasks; using Azure.Storage.Files.DataLake; using ManagedCode.Communication; -using ManagedCode.Storage.AzureDataLake.Options; +using ManagedCode.Storage.Azure.DataLake.Options; using ManagedCode.Storage.Core; -namespace ManagedCode.Storage.AzureDataLake; +namespace ManagedCode.Storage.Azure.DataLake; public interface IAzureDataLakeStorage : IStorage { diff --git a/ManagedCode.Storage.AzureDataLake/ManagedCode.Storage.AzureDataLake.csproj b/ManagedCode.Storage.Azure.DataLake/ManagedCode.Storage.Azure.DataLake.csproj similarity index 88% rename from ManagedCode.Storage.AzureDataLake/ManagedCode.Storage.AzureDataLake.csproj rename to ManagedCode.Storage.Azure.DataLake/ManagedCode.Storage.Azure.DataLake.csproj index ddbfda76..30bccec4 100644 --- a/ManagedCode.Storage.AzureDataLake/ManagedCode.Storage.AzureDataLake.csproj +++ b/ManagedCode.Storage.Azure.DataLake/ManagedCode.Storage.Azure.DataLake.csproj @@ -12,7 +12,7 @@ ManagedCode.Storage.AzureDataLake - ManagedCode.Storage.AzureDataLake + ManagedCode.Storage.Azure.DataLake Storage for AzureDataLake managedcode, azure, storage, cloud, blob, datalake, data, lake @@ -22,11 +22,11 @@ - - - + + + - + diff --git a/ManagedCode.Storage.Azure.DataLake/Options/AzureDataLakeStorageOptions.cs b/ManagedCode.Storage.Azure.DataLake/Options/AzureDataLakeStorageOptions.cs new file mode 100644 index 00000000..277f83e4 --- /dev/null +++ b/ManagedCode.Storage.Azure.DataLake/Options/AzureDataLakeStorageOptions.cs @@ -0,0 +1,17 @@ +using Azure.Storage.Files.DataLake.Models; +using ManagedCode.Storage.Core; + +namespace ManagedCode.Storage.Azure.DataLake.Options; + +public class AzureDataLakeStorageOptions : IStorageOptions +{ + public string ConnectionString { get; set; } + public string FileSystem { get; set; } + + public bool CreateContainerIfNotExists { get; set; } = true; + + public DataLakeFileSystemCreateOptions PublicAccessType { get; set; } = new() + { + PublicAccessType = global::Azure.Storage.Files.DataLake.Models.PublicAccessType.None + }; +} \ No newline at end of file diff --git a/ManagedCode.Storage.AzureDataLake/Options/OpenReadStreamOptions.cs b/ManagedCode.Storage.Azure.DataLake/Options/OpenReadStreamOptions.cs similarity index 76% rename from ManagedCode.Storage.AzureDataLake/Options/OpenReadStreamOptions.cs rename to ManagedCode.Storage.Azure.DataLake/Options/OpenReadStreamOptions.cs index 67537b20..4c8b4ba6 100644 --- a/ManagedCode.Storage.AzureDataLake/Options/OpenReadStreamOptions.cs +++ b/ManagedCode.Storage.Azure.DataLake/Options/OpenReadStreamOptions.cs @@ -1,6 +1,6 @@ using ManagedCode.Storage.Core.Models; -namespace ManagedCode.Storage.AzureDataLake.Options; +namespace ManagedCode.Storage.Azure.DataLake.Options; public class OpenReadStreamOptions : BaseOptions { diff --git a/ManagedCode.Storage.AzureDataLake/Options/OpenWriteStreamOptions.cs b/ManagedCode.Storage.Azure.DataLake/Options/OpenWriteStreamOptions.cs similarity index 71% rename from ManagedCode.Storage.AzureDataLake/Options/OpenWriteStreamOptions.cs rename to ManagedCode.Storage.Azure.DataLake/Options/OpenWriteStreamOptions.cs index 21ea0084..7bb1b7c9 100644 --- a/ManagedCode.Storage.AzureDataLake/Options/OpenWriteStreamOptions.cs +++ b/ManagedCode.Storage.Azure.DataLake/Options/OpenWriteStreamOptions.cs @@ -1,6 +1,6 @@ using ManagedCode.Storage.Core.Models; -namespace ManagedCode.Storage.AzureDataLake.Options; +namespace ManagedCode.Storage.Azure.DataLake.Options; public class OpenWriteStreamOptions : BaseOptions { diff --git a/ManagedCode.Storage.Azure/AzureStorage.cs b/ManagedCode.Storage.Azure/AzureStorage.cs index 754ba868..63bcc708 100644 --- a/ManagedCode.Storage.Azure/AzureStorage.cs +++ b/ManagedCode.Storage.Azure/AzureStorage.cs @@ -3,9 +3,11 @@ using System.IO; using System.Linq; using System.Runtime.CompilerServices; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Azure; +using Azure.Identity; using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Azure.Storage.Blobs.Specialized; @@ -17,11 +19,11 @@ namespace ManagedCode.Storage.Azure; -public class AzureStorage : BaseStorage, IAzureStorage +public class AzureStorage : BaseStorage, IAzureStorage { private readonly ILogger? _logger; - public AzureStorage(AzureStorageOptions options, ILogger? logger = null) : base(options) + public AzureStorage(IAzureStorageOptions options, ILogger? logger = null) : base(options) { _logger = logger; } @@ -36,7 +38,7 @@ public override async Task RemoveContainerAsync(CancellationToken cancel } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -69,7 +71,8 @@ public override async IAsyncEnumerable GetBlobMetadataListAsync(st } } - public async Task> OpenReadStreamAsync(string fileName, CancellationToken cancellationToken = default) + public async Task> OpenReadStreamAsync(string fileName, + CancellationToken cancellationToken = default) { try { @@ -80,12 +83,13 @@ public async Task> OpenReadStreamAsync(string fileName, Cancellat } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } - public async Task> OpenWriteStreamAsync(string fileName, CancellationToken cancellationToken = default) + public async Task> OpenWriteStreamAsync(string fileName, + CancellationToken cancellationToken = default) { try { @@ -96,7 +100,7 @@ public async Task> OpenWriteStreamAsync(string fileName, Cancella } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -113,24 +117,36 @@ public Stream GetBlobStream(string fileName, bool userBuffer = true, int bufferS protected override BlobContainerClient CreateStorageClient() { - return new BlobContainerClient( - StorageOptions.ConnectionString, - StorageOptions.Container, - StorageOptions.OriginalOptions - ); + return StorageOptions switch + { + AzureStorageOptions azureStorageOptions => new BlobContainerClient( + azureStorageOptions.ConnectionString, + azureStorageOptions.Container, + azureStorageOptions.OriginalOptions + ), + + AzureStorageCredentialsOptions azureStorageCredentialsOptions => new BlobContainerClient( + new Uri( + $"https://{azureStorageCredentialsOptions.AccountName}.blob.core.windows.net/{azureStorageCredentialsOptions.ContainerName}"), + azureStorageCredentialsOptions.Credentials, + azureStorageCredentialsOptions.OriginalOptions + ), + }; } protected override async Task CreateContainerInternalAsync(CancellationToken cancellationToken = default) { try { - _ = await StorageClient.CreateIfNotExistsAsync(PublicAccessType.BlobContainer, cancellationToken: cancellationToken); + _ = await StorageClient.CreateIfNotExistsAsync(PublicAccessType.BlobContainer, + cancellationToken: cancellationToken); var policy = await StorageClient.GetAccessPolicyAsync(cancellationToken: cancellationToken); if (policy.Value.BlobPublicAccess != StorageOptions.PublicAccessType) { - await StorageClient.SetAccessPolicyAsync(StorageOptions.PublicAccessType, cancellationToken: cancellationToken); + await StorageClient.SetAccessPolicyAsync(StorageOptions.PublicAccessType, + cancellationToken: cancellationToken); } - + IsContainerCreated = true; return Result.Succeed(); @@ -138,12 +154,13 @@ protected override async Task CreateContainerInternalAsync(CancellationT catch (Exception ex) { IsContainerCreated = false; - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } - protected override async Task DeleteDirectoryInternalAsync(string directory, CancellationToken cancellationToken = default) + protected override async Task DeleteDirectoryInternalAsync(string directory, + CancellationToken cancellationToken = default) { try { @@ -159,7 +176,7 @@ protected override async Task DeleteDirectoryInternalAsync(string direct } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -188,7 +205,7 @@ protected override async Task> UploadInternalAsync(Stream s } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -222,12 +239,13 @@ protected override async Task> DownloadInternalAsync(LocalFile } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } - protected override async Task> DeleteInternalAsync(DeleteOptions options, CancellationToken cancellationToken = default) + protected override async Task> DeleteInternalAsync(DeleteOptions options, + CancellationToken cancellationToken = default) { try { @@ -243,12 +261,13 @@ protected override async Task> DeleteInternalAsync(DeleteOptions op } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } - protected override async Task> ExistsInternalAsync(ExistOptions options, CancellationToken cancellationToken = default) + protected override async Task> ExistsInternalAsync(ExistOptions options, + CancellationToken cancellationToken = default) { try { @@ -259,7 +278,7 @@ protected override async Task> ExistsInternalAsync(ExistOptions opt } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -293,7 +312,7 @@ protected override async Task> GetBlobMetadataInternalAsync } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -312,12 +331,13 @@ protected override async Task SetLegalHoldInternalAsync(bool hasLegalHol } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } - protected override async Task> HasLegalHoldInternalAsync(LegalHoldOptions options, CancellationToken cancellationToken = default) + protected override async Task> HasLegalHoldInternalAsync(LegalHoldOptions options, + CancellationToken cancellationToken = default) { try { @@ -328,8 +348,30 @@ protected override async Task> HasLegalHoldInternalAsync(LegalHoldO } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } + + public async Task SetStorageOptions(IStorageOptions options, CancellationToken cancellationToken = default) + { + StorageOptions = options as IAzureStorageOptions; + + StorageClient = CreateStorageClient(); + return await CreateContainerAsync(cancellationToken); + } + + public async Task SetStorageOptions(Action options, + CancellationToken cancellationToken = default) + { + var type = options.GetType().GetGenericArguments()[0]; + + StorageOptions = JsonSerializer.Deserialize(JsonSerializer.Serialize(StorageOptions), type) as IAzureStorageOptions; + + options.Invoke(StorageOptions); + + StorageClient = CreateStorageClient(); + + return await CreateContainerAsync(cancellationToken); + } } \ No newline at end of file diff --git a/ManagedCode.Storage.Azure/Extensions/ServiceCollectionExtensions.cs b/ManagedCode.Storage.Azure/Extensions/ServiceCollectionExtensions.cs index 49f43afd..e4d457ae 100644 --- a/ManagedCode.Storage.Azure/Extensions/ServiceCollectionExtensions.cs +++ b/ManagedCode.Storage.Azure/Extensions/ServiceCollectionExtensions.cs @@ -8,7 +8,8 @@ namespace ManagedCode.Storage.Azure.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddAzureStorage(this IServiceCollection serviceCollection, Action action) + public static IServiceCollection AddAzureStorage(this IServiceCollection serviceCollection, + Action action) { var options = new AzureStorageOptions(); action.Invoke(options); @@ -18,7 +19,8 @@ public static IServiceCollection AddAzureStorage(this IServiceCollection service return serviceCollection.AddAzureStorage(options); } - public static IServiceCollection AddAzureStorageAsDefault(this IServiceCollection serviceCollection, Action action) + public static IServiceCollection AddAzureStorageAsDefault(this IServiceCollection serviceCollection, + Action action) { var options = new AzureStorageOptions(); action.Invoke(options); @@ -28,14 +30,38 @@ public static IServiceCollection AddAzureStorageAsDefault(this IServiceCollectio return serviceCollection.AddAzureStorageAsDefault(options); } - public static IServiceCollection AddAzureStorage(this IServiceCollection serviceCollection, AzureStorageOptions options) + public static IServiceCollection AddAzureStorage(this IServiceCollection serviceCollection, + Action action) + { + var options = new AzureStorageCredentialsOptions(); + action.Invoke(options); + + CheckConfiguration(options); + + return serviceCollection.AddAzureStorage(options); + } + + public static IServiceCollection AddAzureStorageAsDefault(this IServiceCollection serviceCollection, + Action action) + { + var options = new AzureStorageCredentialsOptions(); + action.Invoke(options); + + CheckConfiguration(options); + + return serviceCollection.AddAzureStorageAsDefault(options); + } + + public static IServiceCollection AddAzureStorage(this IServiceCollection serviceCollection, + IAzureStorageOptions options) { CheckConfiguration(options); serviceCollection.AddSingleton(options); return serviceCollection.AddTransient(); } - public static IServiceCollection AddAzureStorageAsDefault(this IServiceCollection serviceCollection, AzureStorageOptions options) + public static IServiceCollection AddAzureStorageAsDefault(this IServiceCollection serviceCollection, + IAzureStorageOptions options) { CheckConfiguration(options); serviceCollection.AddSingleton(options); @@ -43,16 +69,40 @@ public static IServiceCollection AddAzureStorageAsDefault(this IServiceCollectio return serviceCollection.AddTransient(); } - private static void CheckConfiguration(AzureStorageOptions options) + private static void CheckConfiguration(IAzureStorageOptions options) { - if (string.IsNullOrEmpty(options.ConnectionString)) + if (options is AzureStorageOptions azureStorageOptions) { - throw new BadConfigurationException($"{nameof(options.ConnectionString)} cannot be empty"); + if (string.IsNullOrEmpty(azureStorageOptions.ConnectionString)) + { + throw new BadConfigurationException($"{nameof(azureStorageOptions.ConnectionString)} cannot be empty"); + } + + if (string.IsNullOrEmpty(azureStorageOptions.Container)) + { + throw new BadConfigurationException($"{nameof(azureStorageOptions.Container)} cannot be empty"); + } } - if (string.IsNullOrEmpty(options.Container)) + if (options is AzureStorageCredentialsOptions azureStorageCredentialsOptions) { - throw new BadConfigurationException($"{nameof(options.Container)} cannot be empty"); + if (string.IsNullOrEmpty(azureStorageCredentialsOptions.AccountName)) + { + throw new BadConfigurationException( + $"{nameof(azureStorageCredentialsOptions.AccountName)} cannot be empty"); + } + + if (string.IsNullOrEmpty(azureStorageCredentialsOptions.ContainerName)) + { + throw new BadConfigurationException( + $"{nameof(azureStorageCredentialsOptions.ContainerName)} cannot be empty"); + } + + if (azureStorageCredentialsOptions.Credentials is null) + { + throw new BadConfigurationException( + $"{nameof(azureStorageCredentialsOptions.Credentials)} cannot be null"); + } } } } \ No newline at end of file diff --git a/ManagedCode.Storage.Azure/IAzureStorage.cs b/ManagedCode.Storage.Azure/IAzureStorage.cs index cd0ce52b..68c5c9eb 100644 --- a/ManagedCode.Storage.Azure/IAzureStorage.cs +++ b/ManagedCode.Storage.Azure/IAzureStorage.cs @@ -8,7 +8,7 @@ namespace ManagedCode.Storage.Azure; -public interface IAzureStorage : IStorage +public interface IAzureStorage : IStorage { Task> OpenReadStreamAsync(string fileName, CancellationToken cancellationToken = default); Task> OpenWriteStreamAsync(string fileName, CancellationToken cancellationToken = default); diff --git a/ManagedCode.Storage.Azure/ManagedCode.Storage.Azure.csproj b/ManagedCode.Storage.Azure/ManagedCode.Storage.Azure.csproj index 542dcb26..a88d0bf3 100644 --- a/ManagedCode.Storage.Azure/ManagedCode.Storage.Azure.csproj +++ b/ManagedCode.Storage.Azure/ManagedCode.Storage.Azure.csproj @@ -22,11 +22,12 @@ - - - + + + + - + diff --git a/ManagedCode.Storage.Azure/Options/AzureStorageCredentialsOptions.cs b/ManagedCode.Storage.Azure/Options/AzureStorageCredentialsOptions.cs new file mode 100644 index 00000000..9b646d4d --- /dev/null +++ b/ManagedCode.Storage.Azure/Options/AzureStorageCredentialsOptions.cs @@ -0,0 +1,20 @@ +using Azure.Core; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using ManagedCode.Storage.Core; + +namespace ManagedCode.Storage.Azure.Options; + +public class AzureStorageCredentialsOptions : IAzureStorageOptions +{ + public string AccountName { get; set; } + public string ContainerName { get; set; } + + public TokenCredential Credentials { get; set; } + + public string? Container { get; set; } + public PublicAccessType PublicAccessType { get; set; } + public BlobClientOptions? OriginalOptions { get; set; } + + public bool CreateContainerIfNotExists { get; set; } = true; +} \ No newline at end of file diff --git a/ManagedCode.Storage.Azure/Options/AzureStorageOptions.cs b/ManagedCode.Storage.Azure/Options/AzureStorageOptions.cs index 017268ff..d38abc00 100644 --- a/ManagedCode.Storage.Azure/Options/AzureStorageOptions.cs +++ b/ManagedCode.Storage.Azure/Options/AzureStorageOptions.cs @@ -4,10 +4,12 @@ namespace ManagedCode.Storage.Azure.Options; -public class AzureStorageOptions : StorageOptions +public class AzureStorageOptions : IAzureStorageOptions { public string? ConnectionString { get; set; } public string? Container { get; set; } public PublicAccessType PublicAccessType { get; set; } public BlobClientOptions? OriginalOptions { get; set; } + + public bool CreateContainerIfNotExists { get; set; } = true; } \ No newline at end of file diff --git a/ManagedCode.Storage.Azure/Options/IAzureStorageOptions.cs b/ManagedCode.Storage.Azure/Options/IAzureStorageOptions.cs new file mode 100644 index 00000000..942b328f --- /dev/null +++ b/ManagedCode.Storage.Azure/Options/IAzureStorageOptions.cs @@ -0,0 +1,12 @@ +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using ManagedCode.Storage.Core; + +namespace ManagedCode.Storage.Azure.Options; + +public interface IAzureStorageOptions : IStorageOptions +{ + public string? Container { get; set; } + public PublicAccessType PublicAccessType { get; set; } + public BlobClientOptions? OriginalOptions { get; set; } +} \ No newline at end of file diff --git a/ManagedCode.Storage.AzureDataLake/Options/AzureDataLakeStorageOptions.cs b/ManagedCode.Storage.AzureDataLake/Options/AzureDataLakeStorageOptions.cs deleted file mode 100644 index 58b68eb9..00000000 --- a/ManagedCode.Storage.AzureDataLake/Options/AzureDataLakeStorageOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Azure.Storage.Files.DataLake.Models; -using ManagedCode.Storage.Core; - -namespace ManagedCode.Storage.AzureDataLake.Options; - -public class AzureDataLakeStorageOptions : StorageOptions -{ - public string ConnectionString { get; set; } - public string FileSystem { get; set; } - - public DataLakeFileSystemCreateOptions PublicAccessType { get; set; } = new() - { - PublicAccessType = Azure.Storage.Files.DataLake.Models.PublicAccessType.None - }; -} \ No newline at end of file diff --git a/ManagedCode.Storage.Client.SignalR/Class1.cs b/ManagedCode.Storage.Client.SignalR/Class1.cs new file mode 100644 index 00000000..206aed0a --- /dev/null +++ b/ManagedCode.Storage.Client.SignalR/Class1.cs @@ -0,0 +1,5 @@ +namespace ManagedCode.Storage.Client; + +public class Class1 +{ +} \ No newline at end of file diff --git a/ManagedCode.Storage.Client.SignalR/ManagedCode.Storage.Client.SignalR.csproj b/ManagedCode.Storage.Client.SignalR/ManagedCode.Storage.Client.SignalR.csproj new file mode 100644 index 00000000..bc2f854f --- /dev/null +++ b/ManagedCode.Storage.Client.SignalR/ManagedCode.Storage.Client.SignalR.csproj @@ -0,0 +1,30 @@ + + + + net6.0;net7.0 + 11 + true + embedded + enable + true + Library + + + + + ManagedCode.Storage.Client.SignalR + MManagedCode.Storage.Client.SignalR + Extensions for ASP.NET for Storage + managedcode, aws, gcp, azure storage, cloud, asp.net, file, upload, download + + + + + + + + + + + + \ No newline at end of file diff --git a/ManagedCode.Storage.Client/Class1.cs b/ManagedCode.Storage.Client/Class1.cs new file mode 100644 index 00000000..206aed0a --- /dev/null +++ b/ManagedCode.Storage.Client/Class1.cs @@ -0,0 +1,5 @@ +namespace ManagedCode.Storage.Client; + +public class Class1 +{ +} \ No newline at end of file diff --git a/ManagedCode.Storage.Client/ManagedCode.Storage.Client.csproj b/ManagedCode.Storage.Client/ManagedCode.Storage.Client.csproj new file mode 100644 index 00000000..845bdd74 --- /dev/null +++ b/ManagedCode.Storage.Client/ManagedCode.Storage.Client.csproj @@ -0,0 +1,29 @@ + + + + net6.0;net7.0 + 11 + true + embedded + enable + true + Library + + + + + ManagedCode.Storage.Client + ManagedCode.Storage.Client + Extensions for ASP.NET for Storage + managedcode, aws, gcp, azure storage, cloud, asp.net, file, upload, download + + + + + + + + + + + \ No newline at end of file diff --git a/ManagedCode.Storage.Core/BaseStorage.cs b/ManagedCode.Storage.Core/BaseStorage.cs index 008213cd..0e3fa739 100644 --- a/ManagedCode.Storage.Core/BaseStorage.cs +++ b/ManagedCode.Storage.Core/BaseStorage.cs @@ -12,7 +12,7 @@ namespace ManagedCode.Storage.Core; -public abstract class BaseStorage : IStorage where TOptions : StorageOptions +public abstract class BaseStorage : IStorage where TOptions : IStorageOptions { protected bool IsContainerCreated; protected TOptions StorageOptions; diff --git a/ManagedCode.Storage.Core/IStorage.cs b/ManagedCode.Storage.Core/IStorage.cs index f4042b02..38e12144 100644 --- a/ManagedCode.Storage.Core/IStorage.cs +++ b/ManagedCode.Storage.Core/IStorage.cs @@ -8,7 +8,7 @@ namespace ManagedCode.Storage.Core; -public interface IStorage : IStorage where TOptions : StorageOptions +public interface IStorage : IStorage where TOptions : IStorageOptions { T StorageClient { get; } Task SetStorageOptions(TOptions options, CancellationToken cancellationToken = default); diff --git a/ManagedCode.Storage.Core/LoggerExtensions.cs b/ManagedCode.Storage.Core/LoggerExtensions.cs new file mode 100644 index 00000000..c121e09e --- /dev/null +++ b/ManagedCode.Storage.Core/LoggerExtensions.cs @@ -0,0 +1,13 @@ +using System; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Logging; + +namespace ManagedCode.Storage.Core; + +public static class LoggerExtensions +{ + public static void LogException(this ILogger? logger, Exception ex, [CallerMemberName] string methodName = null) + { + logger?.LogError(ex, methodName); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Core/ManagedCode.Storage.Core.csproj b/ManagedCode.Storage.Core/ManagedCode.Storage.Core.csproj index 6e583547..5eeb2668 100644 --- a/ManagedCode.Storage.Core/ManagedCode.Storage.Core.csproj +++ b/ManagedCode.Storage.Core/ManagedCode.Storage.Core.csproj @@ -18,10 +18,10 @@ - + - + diff --git a/ManagedCode.Storage.Core/Prototype.cs b/ManagedCode.Storage.Core/Prototype.cs new file mode 100644 index 00000000..da14cf73 --- /dev/null +++ b/ManagedCode.Storage.Core/Prototype.cs @@ -0,0 +1,227 @@ +// using System; +// using System.Collections.Generic; +// using System.IO; +// using System.Threading; +// using System.Threading.Tasks; +// using ManagedCode.Communication; +// using ManagedCode.Storage.Core.Models; +// +// namespace ManageCode.FileStream.Client.Abstractions; +// public interface IFileEndpoint +// { +// +// +// public Task UploadAsync(string[] filesPath); +// } +// +// public class Prog +// { +// public void Do() +// { +// IFileClient client; +// +// var a = client.UploadAsync(x => x.FromPath("file path")); +// } +// } +// +// public class FileUploadExtensions +// { +// +// /// +// /// Upload data from the stream into the blob storage. +// /// +// Task> UploadAsync(Stream stream, CancellationToken cancellationToken = default); +// +// /// +// /// Upload array of bytes into the blob storage. +// /// +// Task> UploadAsync(byte[] data, CancellationToken cancellationToken = default); +// +// /// +// /// Upload data from the string into the blob storage. +// /// +// Task> UploadAsync(string content, CancellationToken cancellationToken = default); +// +// /// +// /// Upload data from the file into the blob storage. +// /// +// Task> UploadAsync(FileInfo fileInfo, CancellationToken cancellationToken = default); +// +// /// +// /// Upload data from the stream into the blob storage. +// /// +// Task> UploadAsync(Stream stream, UploadOptions options, CancellationToken cancellationToken = default); +// +// /// +// /// Upload array of bytes into the blob storage. +// /// +// Task> UploadAsync(byte[] data, UploadOptions options, CancellationToken cancellationToken = default); +// +// /// +// /// Upload data from the string into the blob storage. +// /// +// Task> UploadAsync(string content, UploadOptions options, CancellationToken cancellationToken = default); +// +// /// +// /// Upload data from the file into the blob storage. +// /// +// Task> UploadAsync(FileInfo fileInfo, UploadOptions options, CancellationToken cancellationToken = default); +// +// /// +// /// Upload data from the stream into the blob storage. +// /// +// Task> UploadAsync(Stream stream, Action action, CancellationToken cancellationToken = default); +// +// /// +// /// Upload array of bytes into the blob storage. +// /// +// Task> UploadAsync(byte[] data, Action action, CancellationToken cancellationToken = default); +// +// /// +// /// Upload data from the string into the blob storage. +// /// +// Task> UploadAsync(string content, Action action, CancellationToken cancellationToken = default); +// +// /// +// /// Upload data from the file into the blob storage. +// /// +// Task> UploadAsync(FileInfo fileInfo, Action action, CancellationToken cancellationToken = default); +// } +// +// public class BlobMetaData +// { +// +// } +// +// +// public interface IBlobStorage : +// IFileUploader, +// IFileDownloader, +// IFileDeleter, +// ILegalHold, +// IMetaDataReader, IStorageOptions +// +// { +// public Task IsFileExistsAsync(Action file); +// +// Task CreateContainerAsync(CancellationToken cancellationToken = default); +// +// /// +// /// Delete a container if it does not already exist. +// /// +// Task RemoveContainerAsync(CancellationToken cancellationToken = default); +// +// Task DeleteDirectoryAsync(string directory, CancellationToken cancellationToken = default); +// } +// +// public interface IStorageOptions +// { +// Task SetStorageOptions(TOptions options, CancellationToken cancellationToken = default); +// Task SetStorageOptions(Action options, CancellationToken cancellationToken = default); +// } +// +// public interface IMetaDataReader +// { +// public Task GetMetaDataAsync(Action file, CancellationToken token = default); +// +// IAsyncEnumerable GetBlobMetadataListAsync(string? directory = null, CancellationToken cancellationToken = default); +// } +// +// public interface ILegalHold +// { +// public Task SetLegalHoldAsync(Action file, bool legalHoldStatus, CancellationToken cancellationToken = default); +// +// public Task HasLegalHold(Action file, CancellationToken cancellationToken = default); +// } +// +// public interface IFileUploader +// where TOptions : class +// { +// public Task UploadAsync(Action file, TOptions? options = null, +// ProgressHandler? progressHandler = null, CancellationToken? token = null); +// +// } +// +// +// public interface IFileDownloader +// where TOptions : class +// { +// public Task DownloadAsync(Action fileChooser, TOptions? options = null, +// ProgressHandler? progressHandler = null, CancellationToken? token = null); +// } +// +// public interface IFileDeleter +// where TOptions : class +// { +// public Task DeleteAsync(Action file, TOptions? options = null, CancellationToken? token = null); +// } +// +// +// public interface IFileChooser +// { +// public IFileChooser FromUrl(string url); +// +// public void FromDirectory(string directory, string fileName); +// } +// +// public class DownloadOptions +// { +// +// } +// +// public class UploadOptions +// { +// +// } +// +// public class UploadResult +// { +// +// } +// +// public delegate void ProgressHandler(object sender, ProgressArgs args); +// +// public class ProgressArgs +// { +// +// } +// +// +// public interface IFileClient : IFileUploader, IFileDownloader +// { +// +// } +// +// public interface IFileReader +// { +// public void FromPath(string filePath); +// +// public void FromFileInfo(FileInfo info); +// +// public void FromStream(Stream stream); +// +// public void FromBytes(byte[] bytes); +// } +// +// internal class FileReader : IFileReader +// { +// public void FromPath(string filePath) +// { +// throw new NotImplementedException(); +// } +// +// public void FromFileInfo(FileInfo info) +// { +// throw new NotImplementedException(); +// } +// +// public void FromStream(Stream stream) +// { +// throw new NotImplementedException(); +// } +// +// public void FromBytes(byte[] bytes) +// { +// throw new NotImplementedException(); +// } +// } \ No newline at end of file diff --git a/ManagedCode.Storage.Core/StorageOptions.cs b/ManagedCode.Storage.Core/StorageOptions.cs index 2142378d..20fdbd7b 100644 --- a/ManagedCode.Storage.Core/StorageOptions.cs +++ b/ManagedCode.Storage.Core/StorageOptions.cs @@ -1,6 +1,6 @@ namespace ManagedCode.Storage.Core; -public class StorageOptions +public interface IStorageOptions { - public bool CreateContainerIfNotExists { get; set; } = true; + public bool CreateContainerIfNotExists { get; set; } } \ No newline at end of file diff --git a/ManagedCode.Storage.FileSystem/ManagedCode.Storage.FileSystem.csproj b/ManagedCode.Storage.FileSystem/ManagedCode.Storage.FileSystem.csproj index 0ec1c5fa..2d9d6c50 100644 --- a/ManagedCode.Storage.FileSystem/ManagedCode.Storage.FileSystem.csproj +++ b/ManagedCode.Storage.FileSystem/ManagedCode.Storage.FileSystem.csproj @@ -22,7 +22,7 @@ - + diff --git a/ManagedCode.Storage.FileSystem/Options/FileSystemStorageOptions.cs b/ManagedCode.Storage.FileSystem/Options/FileSystemStorageOptions.cs index 1f552855..98886f09 100644 --- a/ManagedCode.Storage.FileSystem/Options/FileSystemStorageOptions.cs +++ b/ManagedCode.Storage.FileSystem/Options/FileSystemStorageOptions.cs @@ -2,7 +2,9 @@ namespace ManagedCode.Storage.FileSystem.Options; -public class FileSystemStorageOptions : StorageOptions +public class FileSystemStorageOptions : IStorageOptions { public string? BaseFolder { get; set; } + + public bool CreateContainerIfNotExists { get; set; } = true; } \ No newline at end of file diff --git a/ManagedCode.Storage.Gcp/Extensions/ServiceCollectionExtensions.cs b/ManagedCode.Storage.Google/Extensions/ServiceCollectionExtensions.cs similarity index 96% rename from ManagedCode.Storage.Gcp/Extensions/ServiceCollectionExtensions.cs rename to ManagedCode.Storage.Google/Extensions/ServiceCollectionExtensions.cs index 1d84aaab..6b9ad5ff 100644 --- a/ManagedCode.Storage.Gcp/Extensions/ServiceCollectionExtensions.cs +++ b/ManagedCode.Storage.Google/Extensions/ServiceCollectionExtensions.cs @@ -1,10 +1,10 @@ using System; using ManagedCode.Storage.Core; using ManagedCode.Storage.Core.Exceptions; -using ManagedCode.Storage.Gcp.Options; +using ManagedCode.Storage.Google.Options; using Microsoft.Extensions.DependencyInjection; -namespace ManagedCode.Storage.Gcp.Extensions; +namespace ManagedCode.Storage.Google.Extensions; public static class ServiceCollectionExtensions { diff --git a/ManagedCode.Storage.Gcp/GCPStorage.cs b/ManagedCode.Storage.Google/GCPStorage.cs similarity index 94% rename from ManagedCode.Storage.Gcp/GCPStorage.cs rename to ManagedCode.Storage.Google/GCPStorage.cs index b1b9985c..825894d3 100644 --- a/ManagedCode.Storage.Gcp/GCPStorage.cs +++ b/ManagedCode.Storage.Google/GCPStorage.cs @@ -11,10 +11,10 @@ using ManagedCode.Communication; using ManagedCode.Storage.Core; using ManagedCode.Storage.Core.Models; -using ManagedCode.Storage.Gcp.Options; +using ManagedCode.Storage.Google.Options; using Microsoft.Extensions.Logging; -namespace ManagedCode.Storage.Gcp; +namespace ManagedCode.Storage.Google; public class GCPStorage : BaseStorage, IGCPStorage { @@ -34,7 +34,7 @@ public override async Task RemoveContainerAsync(CancellationToken cancel } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -106,7 +106,7 @@ await StorageClient.CreateBucketAsync(StorageOptions.BucketOptions.ProjectId, St } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -129,7 +129,7 @@ protected override async Task DeleteDirectoryInternalAsync(string direct } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -149,7 +149,7 @@ await StorageClient.UploadObjectAsync(StorageOptions.BucketOptions.Bucket, optio } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -168,7 +168,7 @@ await StorageClient.DownloadObjectAsync(StorageOptions.BucketOptions.Bucket, opt } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -188,7 +188,7 @@ protected override async Task> DeleteInternalAsync(DeleteOptions op } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -207,7 +207,7 @@ protected override async Task> ExistsInternalAsync(ExistOptions opt } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -233,7 +233,7 @@ protected override async Task> GetBlobMetadataInternalAsync } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -256,7 +256,7 @@ protected override async Task SetLegalHoldInternalAsync(bool hasLegalHol } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } @@ -274,7 +274,7 @@ protected override async Task> HasLegalHoldInternalAsync(LegalHoldO } catch (Exception ex) { - _logger?.LogError(ex.Message, ex); + _logger.LogException(ex); return Result.Fail(ex); } } diff --git a/ManagedCode.Storage.Gcp/IGCPStorage.cs b/ManagedCode.Storage.Google/IGCPStorage.cs similarity index 63% rename from ManagedCode.Storage.Gcp/IGCPStorage.cs rename to ManagedCode.Storage.Google/IGCPStorage.cs index 2d82b17f..6d1650b7 100644 --- a/ManagedCode.Storage.Gcp/IGCPStorage.cs +++ b/ManagedCode.Storage.Google/IGCPStorage.cs @@ -1,8 +1,8 @@ using Google.Cloud.Storage.V1; using ManagedCode.Storage.Core; -using ManagedCode.Storage.Gcp.Options; +using ManagedCode.Storage.Google.Options; -namespace ManagedCode.Storage.Gcp; +namespace ManagedCode.Storage.Google; public interface IGCPStorage : IStorage { diff --git a/ManagedCode.Storage.Gcp/ManagedCode.Storage.Gcp.csproj b/ManagedCode.Storage.Google/ManagedCode.Storage.Google.csproj similarity index 81% rename from ManagedCode.Storage.Gcp/ManagedCode.Storage.Gcp.csproj rename to ManagedCode.Storage.Google/ManagedCode.Storage.Google.csproj index b87b11ff..9042d796 100644 --- a/ManagedCode.Storage.Gcp/ManagedCode.Storage.Gcp.csproj +++ b/ManagedCode.Storage.Google/ManagedCode.Storage.Google.csproj @@ -12,9 +12,9 @@ ManagedCode.Storage.Gcp - ManagedCode.Storage.Gcp - Storage for GCP - managedcode, gcp, storage, cloud, blob + ManagedCode.Storage.Google + Storage for Google Cloud Storage + managedcode, gcs, storage, cloud, blob @@ -24,11 +24,11 @@ - - - + + + - + diff --git a/ManagedCode.Storage.Gcp/Options/BucketOptions.cs b/ManagedCode.Storage.Google/Options/BucketOptions.cs similarity index 69% rename from ManagedCode.Storage.Gcp/Options/BucketOptions.cs rename to ManagedCode.Storage.Google/Options/BucketOptions.cs index ede1799f..fe4bb51f 100644 --- a/ManagedCode.Storage.Gcp/Options/BucketOptions.cs +++ b/ManagedCode.Storage.Google/Options/BucketOptions.cs @@ -1,4 +1,4 @@ -namespace ManagedCode.Storage.Gcp.Options; +namespace ManagedCode.Storage.Google.Options; public class BucketOptions { diff --git a/ManagedCode.Storage.Gcp/Options/GCPStorageOptions.cs b/ManagedCode.Storage.Google/Options/GCPStorageOptions.cs similarity index 70% rename from ManagedCode.Storage.Gcp/Options/GCPStorageOptions.cs rename to ManagedCode.Storage.Google/Options/GCPStorageOptions.cs index 11004bf9..44dd8d56 100644 --- a/ManagedCode.Storage.Gcp/Options/GCPStorageOptions.cs +++ b/ManagedCode.Storage.Google/Options/GCPStorageOptions.cs @@ -2,13 +2,15 @@ using Google.Cloud.Storage.V1; using ManagedCode.Storage.Core; -namespace ManagedCode.Storage.Gcp.Options; +namespace ManagedCode.Storage.Google.Options; -public class GCPStorageOptions : StorageOptions +public class GCPStorageOptions : IStorageOptions { public string AuthFileName { get; set; } = null!; public BucketOptions BucketOptions { get; set; } public GoogleCredential? GoogleCredential { get; set; } public CreateBucketOptions? OriginalOptions { get; set; } public StorageClientBuilder? StorageClientBuilder { get; set; } + + public bool CreateContainerIfNotExists { get; set; } = true; } \ No newline at end of file diff --git a/ManagedCode.Storage.Server/BaseController.cs b/ManagedCode.Storage.Server/BaseController.cs new file mode 100644 index 00000000..79a11dd3 --- /dev/null +++ b/ManagedCode.Storage.Server/BaseController.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using ManagedCode.Communication; +using ManagedCode.Storage.Core; +using ManagedCode.Storage.Core.Models; +using Microsoft.AspNetCore.Components.Forms; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace ManagedCode.Storage.Server; + +public class BaseController : ControllerBase +{ + private readonly IStorage _storage; + + public BaseController(IStorage storage) + { + _storage = storage; + } + + protected async Task>UploadToStorageAsync(IBrowserFile formFile, + UploadOptions? options = null) + { + return await _storage.UploadToStorageAsync(formFile, options); + } + + protected async Task>UploadToStorageAsync(IBrowserFile formFile, + Action options) + { + return await _storage.UploadToStorageAsync(formFile, options); + } + + protected async Task> DownloadAsFileResult(string blobName, CancellationToken cancellationToken = default) + { + return await _storage.DownloadAsFileResult(blobName, cancellationToken); + } + protected async Task> DownloadAsFileResult(BlobMetadata blobMetadata, CancellationToken cancellationToken = default) + { + return await _storage.DownloadAsFileResult(blobMetadata, cancellationToken); + } + + protected async Task> UploadToStorageAsync(IFormFile formFile, + UploadOptions? options = null, + CancellationToken cancellationToken = default) + { + return await _storage.UploadToStorageAsync(formFile, options, cancellationToken); + } + + protected async Task> UploadToStorageAsync(IFormFile formFile, + Action options, + CancellationToken cancellationToken = default) + { + return await _storage.UploadToStorageAsync(formFile, options, cancellationToken); + } + protected async IAsyncEnumerable> UploadToStorageAsync(IFormFileCollection formFiles, + UploadOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var formFile in formFiles) + { + yield return await _storage.UploadToStorageAsync(formFile, options, cancellationToken); + } + } + protected async IAsyncEnumerable> UploadToStorageAsync(IFormFileCollection formFiles, + Action options, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var formFile in formFiles) + { + yield return await _storage.UploadToStorageAsync(formFile, options, cancellationToken); + } + } + +} \ No newline at end of file diff --git a/ManagedCode.Storage.AspNetExtensions/BrowserFileExtensions.cs b/ManagedCode.Storage.Server/BrowserFileExtensions.cs similarity index 91% rename from ManagedCode.Storage.AspNetExtensions/BrowserFileExtensions.cs rename to ManagedCode.Storage.Server/BrowserFileExtensions.cs index a907f16e..0833fcbd 100644 --- a/ManagedCode.Storage.AspNetExtensions/BrowserFileExtensions.cs +++ b/ManagedCode.Storage.Server/BrowserFileExtensions.cs @@ -3,7 +3,7 @@ using ManagedCode.Storage.Core.Models; using Microsoft.AspNetCore.Components.Forms; -namespace ManagedCode.Storage.AspNetExtensions; +namespace ManagedCode.Storage.Server; public static class BrowserFileExtensions { diff --git a/ManagedCode.Storage.AspNetExtensions/FormFileExtensions.cs b/ManagedCode.Storage.Server/FormFileExtensions.cs similarity index 95% rename from ManagedCode.Storage.AspNetExtensions/FormFileExtensions.cs rename to ManagedCode.Storage.Server/FormFileExtensions.cs index f0d4ca05..5b03a858 100644 --- a/ManagedCode.Storage.AspNetExtensions/FormFileExtensions.cs +++ b/ManagedCode.Storage.Server/FormFileExtensions.cs @@ -5,7 +5,7 @@ using ManagedCode.Storage.Core.Models; using Microsoft.AspNetCore.Http; -namespace ManagedCode.Storage.AspNetExtensions; +namespace ManagedCode.Storage.Server; public static class FormFileExtensions { diff --git a/ManagedCode.Storage.AspNetExtensions/ManagedCode.Storage.AspNetExtensions.csproj b/ManagedCode.Storage.Server/ManagedCode.Storage.Server.csproj similarity index 87% rename from ManagedCode.Storage.AspNetExtensions/ManagedCode.Storage.AspNetExtensions.csproj rename to ManagedCode.Storage.Server/ManagedCode.Storage.Server.csproj index be2f605d..956fc77e 100644 --- a/ManagedCode.Storage.AspNetExtensions/ManagedCode.Storage.AspNetExtensions.csproj +++ b/ManagedCode.Storage.Server/ManagedCode.Storage.Server.csproj @@ -12,8 +12,8 @@ - ManagedCode.Storage.AspNetExtensions - ManagedCode.Storage.AspNetExtensions + ManagedCode.Storage.Server + ManagedCode.Storage.Server Extensions for ASP.NET for Storage managedcode, aws, gcp, azure storage, cloud, asp.net, file, upload, download diff --git a/ManagedCode.Storage.Server/Properties/launchSettings.json b/ManagedCode.Storage.Server/Properties/launchSettings.json new file mode 100644 index 00000000..492fcc8f --- /dev/null +++ b/ManagedCode.Storage.Server/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "ManagedCode.Storage.AspNetExtensions": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:59086;http://localhost:59087" + } + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.AspNetExtensions/StorageBrowserFileExtensions.cs b/ManagedCode.Storage.Server/StorageBrowserFileExtensions.cs similarity index 97% rename from ManagedCode.Storage.AspNetExtensions/StorageBrowserFileExtensions.cs rename to ManagedCode.Storage.Server/StorageBrowserFileExtensions.cs index 7467fb2a..68ea660e 100644 --- a/ManagedCode.Storage.AspNetExtensions/StorageBrowserFileExtensions.cs +++ b/ManagedCode.Storage.Server/StorageBrowserFileExtensions.cs @@ -6,7 +6,7 @@ using ManagedCode.Storage.Core.Models; using Microsoft.AspNetCore.Components.Forms; -namespace ManagedCode.Storage.AspNetExtensions; +namespace ManagedCode.Storage.Server; public static class StorageBrowserFileExtensions { diff --git a/ManagedCode.Storage.AspNetExtensions/StorageExtensions.cs b/ManagedCode.Storage.Server/StorageExtensions.cs similarity index 96% rename from ManagedCode.Storage.AspNetExtensions/StorageExtensions.cs rename to ManagedCode.Storage.Server/StorageExtensions.cs index 2840597f..6c01c740 100644 --- a/ManagedCode.Storage.AspNetExtensions/StorageExtensions.cs +++ b/ManagedCode.Storage.Server/StorageExtensions.cs @@ -6,7 +6,7 @@ using ManagedCode.Storage.Core.Models; using Microsoft.AspNetCore.Mvc; -namespace ManagedCode.Storage.AspNetExtensions; +namespace ManagedCode.Storage.Server; public static class StorageExtensions { diff --git a/ManagedCode.Storage.AspNetExtensions/StorageFromFileExtensions.cs b/ManagedCode.Storage.Server/StorageFromFileExtensions.cs similarity index 98% rename from ManagedCode.Storage.AspNetExtensions/StorageFromFileExtensions.cs rename to ManagedCode.Storage.Server/StorageFromFileExtensions.cs index 75260cdf..bb30095b 100644 --- a/ManagedCode.Storage.AspNetExtensions/StorageFromFileExtensions.cs +++ b/ManagedCode.Storage.Server/StorageFromFileExtensions.cs @@ -8,7 +8,7 @@ using ManagedCode.Storage.Core.Models; using Microsoft.AspNetCore.Http; -namespace ManagedCode.Storage.AspNetExtensions; +namespace ManagedCode.Storage.Server; public static class StorageFromFileExtensions { diff --git a/ManagedCode.Storage.TestFakes/FakeAzureDataLakeStorage.cs b/ManagedCode.Storage.TestFakes/FakeAzureDataLakeStorage.cs index aba1dd84..376f995e 100644 --- a/ManagedCode.Storage.TestFakes/FakeAzureDataLakeStorage.cs +++ b/ManagedCode.Storage.TestFakes/FakeAzureDataLakeStorage.cs @@ -1,7 +1,7 @@ using Azure.Storage.Files.DataLake; using ManagedCode.Communication; -using ManagedCode.Storage.AzureDataLake; -using ManagedCode.Storage.AzureDataLake.Options; +using ManagedCode.Storage.Azure.DataLake; +using ManagedCode.Storage.Azure.DataLake.Options; using ManagedCode.Storage.FileSystem; using ManagedCode.Storage.FileSystem.Options; diff --git a/ManagedCode.Storage.TestFakes/FakeAzureStorage.cs b/ManagedCode.Storage.TestFakes/FakeAzureStorage.cs index b09dfdfb..df1b2106 100644 --- a/ManagedCode.Storage.TestFakes/FakeAzureStorage.cs +++ b/ManagedCode.Storage.TestFakes/FakeAzureStorage.cs @@ -2,6 +2,7 @@ using ManagedCode.Communication; using ManagedCode.Storage.Azure; using ManagedCode.Storage.Azure.Options; +using ManagedCode.Storage.Core; using ManagedCode.Storage.FileSystem; using ManagedCode.Storage.FileSystem.Options; @@ -15,12 +16,24 @@ public FakeAzureStorage() : base(new FileSystemStorageOptions()) public BlobContainerClient StorageClient { get; } + public Task SetStorageOptions(IStorageOptions options, CancellationToken cancellationToken = default) + { + return Task.FromResult(Result.Succeed()); + } + + public Task SetStorageOptions(Action options, + CancellationToken cancellationToken = default) + { + return Task.FromResult(Result.Succeed()); + } + public Task SetStorageOptions(AzureStorageOptions options, CancellationToken cancellationToken = default) { return Task.FromResult(Result.Succeed()); } - public Task SetStorageOptions(Action options, CancellationToken cancellationToken = default) + public Task SetStorageOptions(Action options, + CancellationToken cancellationToken = default) { return Task.FromResult(Result.Succeed()); } diff --git a/ManagedCode.Storage.TestFakes/FakeGCPStorage.cs b/ManagedCode.Storage.TestFakes/FakeGCPStorage.cs index 59d8cc17..92a227d8 100644 --- a/ManagedCode.Storage.TestFakes/FakeGCPStorage.cs +++ b/ManagedCode.Storage.TestFakes/FakeGCPStorage.cs @@ -2,14 +2,14 @@ using ManagedCode.Communication; using ManagedCode.Storage.FileSystem; using ManagedCode.Storage.FileSystem.Options; -using ManagedCode.Storage.Gcp; -using ManagedCode.Storage.Gcp.Options; +using ManagedCode.Storage.Google; +using ManagedCode.Storage.Google.Options; namespace ManagedCode.Storage.TestFakes; -public class FakeGCPStorage : FileSystemStorage, IGCPStorage +public class FakeGoogleStorage : FileSystemStorage, IGCPStorage { - public FakeGCPStorage() : base(new FileSystemStorageOptions()) + public FakeGoogleStorage() : base(new FileSystemStorageOptions()) { } diff --git a/ManagedCode.Storage.TestFakes/ManagedCode.Storage.TestFakes.csproj b/ManagedCode.Storage.TestFakes/ManagedCode.Storage.TestFakes.csproj index 2871c45d..e830230e 100644 --- a/ManagedCode.Storage.TestFakes/ManagedCode.Storage.TestFakes.csproj +++ b/ManagedCode.Storage.TestFakes/ManagedCode.Storage.TestFakes.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0;net7.0 enable enable true @@ -16,11 +16,11 @@ - + - + diff --git a/ManagedCode.Storage.TestFakes/MockCollectionExtensions.cs b/ManagedCode.Storage.TestFakes/MockCollectionExtensions.cs index ac623979..dafe565c 100644 --- a/ManagedCode.Storage.TestFakes/MockCollectionExtensions.cs +++ b/ManagedCode.Storage.TestFakes/MockCollectionExtensions.cs @@ -1,8 +1,8 @@ using ManagedCode.Storage.Aws; using ManagedCode.Storage.Azure; -using ManagedCode.Storage.AzureDataLake; +using ManagedCode.Storage.Azure.DataLake; using ManagedCode.Storage.Core; -using ManagedCode.Storage.Gcp; +using ManagedCode.Storage.Google; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -55,18 +55,18 @@ public static IServiceCollection ReplaceAzureStorageAsDefault(this IServiceColle return serviceCollection; } - public static IServiceCollection ReplaceGCPStorageAsDefault(this IServiceCollection serviceCollection) + public static IServiceCollection ReplaceGoogleStorageAsDefault(this IServiceCollection serviceCollection) { - serviceCollection.ReplaceGCPStorage(); + serviceCollection.ReplaceGoogleStorage(); serviceCollection.AddTransient(); return serviceCollection; } - public static IServiceCollection ReplaceGCPStorage(this IServiceCollection serviceCollection) + public static IServiceCollection ReplaceGoogleStorage(this IServiceCollection serviceCollection) { serviceCollection.RemoveAll(); serviceCollection.RemoveAll(); - serviceCollection.AddTransient(); + serviceCollection.AddTransient(); return serviceCollection; } } \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/AspNetExtensions/FormFileExtensionsTests.cs b/ManagedCode.Storage.Tests/AspNetExtensions/FormFileExtensionsTests.cs deleted file mode 100644 index 5c5b607a..00000000 --- a/ManagedCode.Storage.Tests/AspNetExtensions/FormFileExtensionsTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using FluentAssertions; -using ManagedCode.Storage.AspNetExtensions; -using Microsoft.AspNetCore.Http; -using Xunit; - -namespace ManagedCode.Storage.Tests.AspNetExtensions; - -public class FormFileExtensionsTests -{ - [Fact] - public async Task ToLocalFileAsync_SmallFile() - { - // Arrange - const int size = 200 * 1024; // 200 KB - var fileName = FileHelper.GenerateRandomFileName(); - var formFile = FileHelper.GenerateFormFile(fileName, size); - - // Act - var localFile = await formFile.ToLocalFileAsync(); - - // Assert - localFile.FileStream.Length.Should().Be(formFile.Length); - localFile.Name.Should().Be(formFile.FileName); - } - - [Fact] - public async Task ToLocalFileAsync_LargeFile() - { - // Arrange - const int size = 300 * 1024 * 1024; // 300 MB - var fileName = FileHelper.GenerateRandomFileName(); - var formFile = FileHelper.GenerateFormFile(fileName, size); - - // Act - var localFile = await formFile.ToLocalFileAsync(); - - // Assert - localFile.FileStream.Length.Should().Be(formFile.Length); - localFile.Name.Should().Be(formFile.FileName); - } - - [Fact] - public async Task ToLocalFilesAsync_SmallFiles() - { - // Arrange - const int filesCount = 10; - Random random = new(); - FormFileCollection collection = new(); - - for (var i = 0; i < filesCount; i++) - { - var size = random.Next(10, 1000) * 1024; - var fileName = FileHelper.GenerateRandomFileName(); - collection.Add(FileHelper.GenerateFormFile(fileName, size)); - } - - // Act - var localFiles = await collection.ToLocalFilesAsync().ToListAsync(); - - // Assert - localFiles.Count.Should().Be(filesCount); - - for (var i = 0; i < filesCount; i++) - { - localFiles[i].FileStream.Length.Should().Be(collection[i].Length); - localFiles[i].Name.Should().Be(collection[i].FileName); - } - } -} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/AspNetExtensions/StorageExtensionsTests.cs b/ManagedCode.Storage.Tests/AspNetExtensions/StorageExtensionsTests.cs deleted file mode 100644 index c277e2e7..00000000 --- a/ManagedCode.Storage.Tests/AspNetExtensions/StorageExtensionsTests.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using FluentAssertions; -using ManagedCode.MimeTypes; -using ManagedCode.Storage.AspNetExtensions; -using ManagedCode.Storage.Core; -using ManagedCode.Storage.Core.Models; -using ManagedCode.Storage.FileSystem.Extensions; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace ManagedCode.Storage.Tests.AspNetExtensions; - -public class StorageExtensionsTests -{ - public StorageExtensionsTests() - { - ServiceProvider = ConfigureServices(); - Storage = ServiceProvider.GetService()!; - } - - public IStorage Storage { get; } - public ServiceProvider ServiceProvider { get; } - - public static ServiceProvider ConfigureServices() - { - var services = new ServiceCollection(); - - services.AddFileSystemStorageAsDefault(opt => { opt.BaseFolder = Path.Combine(Environment.CurrentDirectory, "managed-code-bucket"); }); - - return services.BuildServiceProvider(); - } - - [Fact] - public async Task UploadToStorageAsync_SmallFile() - { - // Arrange - const int size = 200 * 1024; // 200 KB - var fileName = FileHelper.GenerateRandomFileName(); - var formFile = FileHelper.GenerateFormFile(fileName, size); - - // Act - await Storage.UploadToStorageAsync(formFile); - var localFile = await Storage.DownloadAsync(fileName); - - // Assert - localFile!.Value.FileInfo.Length.Should().Be(formFile.Length); - localFile.Value.Name.Should().Be(formFile.FileName); - - await Storage.DeleteAsync(fileName); - } - - [Fact] - public async Task UploadToStorageAsync_LargeFile() - { - // Arrange - const int size = 50 * 1024 * 1024; // 50 MB - var fileName = FileHelper.GenerateRandomFileName(); - var formFile = FileHelper.GenerateFormFile(fileName, size); - - // Act - var res = await Storage.UploadToStorageAsync(formFile); - var result = await Storage.DownloadAsync(fileName); - - // Assert - result.Value.Name.Should().Be(formFile.FileName); - - await Storage.DeleteAsync(fileName); - } - - [Fact] - public async Task UploadToStorageAsync_WithRandomName() - { - // Arrange - const int size = 200 * 1024; // 200 KB - var fileName = FileHelper.GenerateRandomFileName(); - var formFile = FileHelper.GenerateFormFile(fileName, size); - - // Act - var result = await Storage.UploadToStorageAsync(formFile); - var localFile = await Storage.DownloadAsync(result.Value.Name); - - // Assert - result.IsSuccess.Should().BeTrue(); - localFile.Value.FileInfo.Length.Should().Be(formFile.Length); - localFile.Value.Name.Should().Be(fileName); - - await Storage.DeleteAsync(fileName); - } - - [Fact] - public async Task DownloadAsFileResult_WithFileName() - { - // Arrange - const int size = 200 * 1024; // 200 KB - var fileName = FileHelper.GenerateRandomFileName(); - var localFile = FileHelper.GenerateLocalFile(fileName, size); - - // Act - await Storage.UploadAsync(localFile.FileInfo); - var result = await Storage.DownloadAsFileResult(fileName); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value!.ContentType.Should().Be(MimeHelper.GetMimeType(localFile.FileInfo.Extension)); - result.Value.FileDownloadName.Should().Be(localFile.Name); - - await Storage.DeleteAsync(fileName); - } - - [Fact] - public async Task DownloadAsFileResult_WithBlobMetadata() - { - // Arrange - const int size = 200 * 1024; // 200 KB - var fileName = FileHelper.GenerateRandomFileName(); - var localFile = FileHelper.GenerateLocalFile(fileName, size); - - BlobMetadata blobMetadata = new() { Name = fileName }; - - // Act - await Storage.UploadAsync(localFile.FileInfo, options => { options.FileName = fileName; }); - var result = await Storage.DownloadAsFileResult(fileName); - - // Assert - result.IsSuccess.Should().Be(true); - result.Value!.ContentType.Should().Be(MimeHelper.GetMimeType(localFile.FileInfo.Extension)); - result.Value.FileDownloadName.Should().Be(localFile.Name); - - await Storage.DeleteAsync(fileName); - } - - [Fact] - public async Task DownloadAsFileResult_WithFileName_IfFileDontExist() - { - // Arrange - var fileName = FileHelper.GenerateRandomFileName(); - - // Act - var fileResult = await Storage.DownloadAsFileResult(fileName); - - // Assert - fileResult.IsSuccess.Should().BeFalse(); - } - - [Fact] - public async Task DownloadAsFileResult_WithBlobMetadata_IfFileDontExist() - { - // Arrange - var fileName = FileHelper.GenerateRandomFileName(); - - BlobMetadata blobMetadata = new() { Name = fileName }; - - // Act - var fileResult = await Storage.DownloadAsFileResult(blobMetadata); - - // Assert - fileResult.IsSuccess.Should().BeFalse(); - } -} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/AssemblyInfo.cs b/ManagedCode.Storage.Tests/AssemblyInfo.cs deleted file mode 100644 index 41a898e3..00000000 --- a/ManagedCode.Storage.Tests/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using Xunit; - -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Azure/AzureStorageTests.cs b/ManagedCode.Storage.Tests/Azure/AzureStorageTests.cs deleted file mode 100644 index ab2a0cf4..00000000 --- a/ManagedCode.Storage.Tests/Azure/AzureStorageTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using FluentAssertions; -using ManagedCode.Storage.Azure; -using ManagedCode.Storage.Azure.Extensions; -using ManagedCode.Storage.Azure.Options; -using ManagedCode.Storage.Core; -using ManagedCode.Storage.Core.Exceptions; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace ManagedCode.Storage.Tests.Azure; - -public class AzureStorageTests : StorageBaseTests -{ - protected override ServiceProvider ConfigureServices() - { - var services = new ServiceCollection(); - - services.AddAzureStorageAsDefault(opt => - { - opt.Container = "managed-code-bucket"; - //https://github.com/marketplace/actions/azuright - opt.ConnectionString = - "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://localhost:10000/devstoreaccount1;QueueEndpoint=http://localhost:10001/devstoreaccount1;TableEndpoint=http://localhost:10002/devstoreaccount1;"; - }); - - services.AddAzureStorage(new AzureStorageOptions - { - Container = "managed-code-bucket", - ConnectionString = - "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://localhost:10000/devstoreaccount1;QueueEndpoint=http://localhost:10001/devstoreaccount1;TableEndpoint=http://localhost:10002/devstoreaccount1;" - }); - return services.BuildServiceProvider(); - } - - [Fact] - public void BadConfigurationForStorage_WithoutContainer_ThrowException() - { - var services = new ServiceCollection(); - - Action action = () => services.AddAzureStorage(opt => - { - opt.ConnectionString = - "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://localhost:10000/devstoreaccount1;QueueEndpoint=http://localhost:10001/devstoreaccount1;TableEndpoint=http://localhost:10002/devstoreaccount1;"; - }); - - action.Should().Throw(); - } - - [Fact] - public void BadConfigurationForStorage_WithoutConnectionString_ThrowException() - { - var services = new ServiceCollection(); - - Action action = () => services.AddAzureStorageAsDefault(opt => { opt.Container = "managed-code-bucket"; }); - - action.Should().Throw(); - } - - [Fact] - public void StorageAsDefaultTest() - { - var storage = ServiceProvider.GetService(); - var defaultStorage = ServiceProvider.GetService(); - storage?.GetType().FullName.Should().Be(defaultStorage?.GetType().FullName); - } -} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Common/BaseContainer.cs b/ManagedCode.Storage.Tests/Common/BaseContainer.cs new file mode 100644 index 00000000..24620800 --- /dev/null +++ b/ManagedCode.Storage.Tests/Common/BaseContainer.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using DotNet.Testcontainers.Containers; +using FluentAssertions; +using ManagedCode.Storage.Core; +using ManagedCode.Storage.Core.Models; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace ManagedCode.Storage.Tests.Common; + +public abstract class BaseContainer : IAsyncLifetime where T : IContainer +{ + protected T Container { get; private set; } + protected abstract T Build(); + protected abstract ServiceProvider ConfigureServices(); + + protected IStorage Storage { get; private set; } + protected ServiceProvider ServiceProvider { get; private set; } + + + public async Task InitializeAsync() + { + Container = Build(); + await Container.StartAsync(); + ServiceProvider = ConfigureServices(); + Storage = ServiceProvider.GetService()!; + } + + public Task DisposeAsync() + { + return Container.DisposeAsync().AsTask(); + } + + protected async Task UploadTestFileAsync(string? directory = null) + { + var file = await GetTestFileAsync(); + + UploadOptions options = new() { FileName = file.Name, Directory = directory }; + var result = await Storage.UploadAsync(file.OpenRead(), options); + result.IsSuccess.Should().BeTrue(); + + return file; + } + + protected async Task> UploadTestFileListAsync(string? directory = null, int? count = 10) + { + List fileList = new(); + + for (var i = 0; i < count; i++) + { + var file = await UploadTestFileAsync(directory); + fileList.Add(file); + } + + return fileList; + } + + protected async Task GetTestFileAsync() + { + var fileName = Path.GetTempFileName(); + var fs = File.OpenWrite(fileName); + var sw = new StreamWriter(fs); + + for (var i = 0; i < 1000; i++) + { + await sw.WriteLineAsync(Guid.NewGuid().ToString()); + } + + await sw.DisposeAsync(); + await fs.DisposeAsync(); + + return new FileInfo(fileName); + } +} diff --git a/ManagedCode.Storage.Tests/Common/EmptyContainer.cs b/ManagedCode.Storage.Tests/Common/EmptyContainer.cs new file mode 100644 index 00000000..9b0ab872 --- /dev/null +++ b/ManagedCode.Storage.Tests/Common/EmptyContainer.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Images; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace ManagedCode.Storage.Tests.Common; + +public sealed class EmptyContainer : IContainer +{ + public ValueTask DisposeAsync() + { + return ValueTask.CompletedTask; + } + + public ushort GetMappedPublicPort(int containerPort) + { + throw new NotImplementedException(); + } + + public ushort GetMappedPublicPort(string containerPort) + { + throw new NotImplementedException(); + } + + public Task GetExitCodeAsync(CancellationToken ct = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public Task<(string Stdout, string Stderr)> GetLogsAsync(DateTime since = new DateTime(), DateTime until = new DateTime(), bool timestampsEnabled = true, + CancellationToken ct = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public Task StartAsync(CancellationToken ct = new CancellationToken()) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken ct = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public Task CopyAsync(byte[] fileContent, string filePath, UnixFileModes fileMode = UnixFileModes.None | UnixFileModes.OtherRead | UnixFileModes.GroupRead | UnixFileModes.UserWrite | UnixFileModes.UserRead, + CancellationToken ct = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public Task CopyAsync(string source, string target, UnixFileModes fileMode = UnixFileModes.None | UnixFileModes.OtherRead | UnixFileModes.GroupRead | UnixFileModes.UserWrite | UnixFileModes.UserRead, + CancellationToken ct = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public Task CopyAsync(DirectoryInfo source, string target, UnixFileModes fileMode = UnixFileModes.None | UnixFileModes.OtherRead | UnixFileModes.GroupRead | UnixFileModes.UserWrite | UnixFileModes.UserRead, + CancellationToken ct = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public Task CopyAsync(FileInfo source, string target, UnixFileModes fileMode = UnixFileModes.None | UnixFileModes.OtherRead | UnixFileModes.GroupRead | UnixFileModes.UserWrite | UnixFileModes.UserRead, + CancellationToken ct = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public Task ReadFileAsync(string filePath, CancellationToken ct = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public Task ExecAsync(IList command, CancellationToken ct = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public ILogger Logger { get; } = NullLogger.Instance; + public string Id { get; } = "none"; + public string Name { get; } = "none"; + public string IpAddress { get; } = "none"; + public string MacAddress { get; } = "none"; + public string Hostname { get; } = "none"; + public IImage Image { get; } = new DockerImage("none"); + public TestcontainersStates State { get; } = TestcontainersStates.Running; + public TestcontainersHealthStatus Health { get; } = TestcontainersHealthStatus.Healthy; + public long HealthCheckFailingStreak { get; } = 0; + public event EventHandler? Creating; + public event EventHandler? Starting; + public event EventHandler? Stopping; + public event EventHandler? Created; + public event EventHandler? Started; + public event EventHandler? Stopped; +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/FileHelper.cs b/ManagedCode.Storage.Tests/Common/FileHelper.cs similarity index 91% rename from ManagedCode.Storage.Tests/FileHelper.cs rename to ManagedCode.Storage.Tests/Common/FileHelper.cs index 5dd5e772..4d5c8c7f 100644 --- a/ManagedCode.Storage.Tests/FileHelper.cs +++ b/ManagedCode.Storage.Tests/Common/FileHelper.cs @@ -5,7 +5,7 @@ using ManagedCode.Storage.Core.Models; using Microsoft.AspNetCore.Http; -namespace ManagedCode.Storage.Tests; +namespace ManagedCode.Storage.Tests.Common; public static class FileHelper { @@ -49,7 +49,7 @@ public static string GenerateRandomFileName(string extension = "txt") public static string GenerateRandomFileContent() { - const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789_abcdefghijklmnopqrstuvwxyz"; return new string(Enumerable.Repeat(chars, 250_000) .Select(s => s[Random.Next(s.Length)]) diff --git a/ManagedCode.Storage.Tests/ExtensionsTests/FormFileExtensionsTests.cs b/ManagedCode.Storage.Tests/ExtensionsTests/FormFileExtensionsTests.cs new file mode 100644 index 00000000..c361be70 --- /dev/null +++ b/ManagedCode.Storage.Tests/ExtensionsTests/FormFileExtensionsTests.cs @@ -0,0 +1,72 @@ +// using System; +// using System.Linq; +// using System.Threading.Tasks; +// using FluentAssertions; +// using ManagedCode.Storage.Server; +// using Microsoft.AspNetCore.Http; +// using Xunit; +// +// namespace ManagedCode.Storage.Tests.AspNetExtensions; +// +// public class FormFileExtensionsTests +// { +// [Fact] +// public async Task ToLocalFileAsync_SmallFile() +// { +// // Arrange +// const int size = 200 * 1024; // 200 KB +// var fileName = FileHelper.GenerateRandomFileName(); +// var formFile = FileHelper.GenerateFormFile(fileName, size); +// +// // Act +// var localFile = await formFile.ToLocalFileAsync(); +// +// // Assert +// localFile.FileStream.Length.Should().Be(formFile.Length); +// localFile.Name.Should().Be(formFile.FileName); +// } +// +// [Fact] +// public async Task ToLocalFileAsync_LargeFile() +// { +// // Arrange +// const int size = 300 * 1024 * 1024; // 300 MB +// var fileName = FileHelper.GenerateRandomFileName(); +// var formFile = FileHelper.GenerateFormFile(fileName, size); +// +// // Act +// var localFile = await formFile.ToLocalFileAsync(); +// +// // Assert +// localFile.FileStream.Length.Should().Be(formFile.Length); +// localFile.Name.Should().Be(formFile.FileName); +// } +// +// [Fact] +// public async Task ToLocalFilesAsync_SmallFiles() +// { +// // Arrange +// const int filesCount = 10; +// Random random = new(); +// FormFileCollection collection = new(); +// +// for (var i = 0; i < filesCount; i++) +// { +// var size = random.Next(10, 1000) * 1024; +// var fileName = FileHelper.GenerateRandomFileName(); +// collection.Add(FileHelper.GenerateFormFile(fileName, size)); +// } +// +// // Act +// var localFiles = await collection.ToLocalFilesAsync().ToListAsync(); +// +// // Assert +// localFiles.Count.Should().Be(filesCount); +// +// for (var i = 0; i < filesCount; i++) +// { +// localFiles[i].FileStream.Length.Should().Be(collection[i].Length); +// localFiles[i].Name.Should().Be(collection[i].FileName); +// } +// } +// } \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/ReplaceExtensionsTests.cs b/ManagedCode.Storage.Tests/ExtensionsTests/ReplaceExtensionsTests.cs similarity index 96% rename from ManagedCode.Storage.Tests/ReplaceExtensionsTests.cs rename to ManagedCode.Storage.Tests/ExtensionsTests/ReplaceExtensionsTests.cs index 0ce8b559..70179a76 100644 --- a/ManagedCode.Storage.Tests/ReplaceExtensionsTests.cs +++ b/ManagedCode.Storage.Tests/ExtensionsTests/ReplaceExtensionsTests.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace ManagedCode.Storage.Tests; +namespace ManagedCode.Storage.Tests.ExtensionsTests; public class ReplaceExtensionsTests { diff --git a/ManagedCode.Storage.Tests/ExtensionsTests/StorageExtensionsTests.cs b/ManagedCode.Storage.Tests/ExtensionsTests/StorageExtensionsTests.cs new file mode 100644 index 00000000..9315ad4b --- /dev/null +++ b/ManagedCode.Storage.Tests/ExtensionsTests/StorageExtensionsTests.cs @@ -0,0 +1,161 @@ +// using System; +// using System.IO; +// using System.Threading.Tasks; +// using FluentAssertions; +// using ManagedCode.MimeTypes; +// using ManagedCode.Storage.Server; +// using ManagedCode.Storage.Core; +// using ManagedCode.Storage.Core.Models; +// using ManagedCode.Storage.FileSystem.Extensions; +// using Microsoft.Extensions.DependencyInjection; +// using Xunit; +// +// namespace ManagedCode.Storage.Tests.AspNetExtensions; +// +// public class StorageExtensionsTests +// { +// public StorageExtensionsTests() +// { +// ServiceProvider = ConfigureServices(); +// Storage = ServiceProvider.GetService()!; +// } +// +// public IStorage Storage { get; } +// public ServiceProvider ServiceProvider { get; } +// +// public static ServiceProvider ConfigureServices() +// { +// var services = new ServiceCollection(); +// +// services.AddFileSystemStorageAsDefault(opt => { opt.BaseFolder = Path.Combine(Environment.CurrentDirectory, "managed-code-bucket"); }); +// +// return services.BuildServiceProvider(); +// } +// +// [Fact] +// public async Task UploadToStorageAsync_SmallFile() +// { +// // Arrange +// const int size = 200 * 1024; // 200 KB +// var fileName = FileHelper.GenerateRandomFileName(); +// var formFile = FileHelper.GenerateFormFile(fileName, size); +// +// // Act +// await Storage.UploadToStorageAsync(formFile); +// var localFile = await Storage.DownloadAsync(fileName); +// +// // Assert +// localFile!.Value.FileInfo.Length.Should().Be(formFile.Length); +// localFile.Value.Name.Should().Be(formFile.FileName); +// +// await Storage.DeleteAsync(fileName); +// } +// +// [Fact] +// public async Task UploadToStorageAsync_LargeFile() +// { +// // Arrange +// const int size = 50 * 1024 * 1024; // 50 MB +// var fileName = FileHelper.GenerateRandomFileName(); +// var formFile = FileHelper.GenerateFormFile(fileName, size); +// +// // Act +// var res = await Storage.UploadToStorageAsync(formFile); +// var result = await Storage.DownloadAsync(fileName); +// +// // Assert +// result.Value.Name.Should().Be(formFile.FileName); +// +// await Storage.DeleteAsync(fileName); +// } +// +// [Fact] +// public async Task UploadToStorageAsync_WithRandomName() +// { +// // Arrange +// const int size = 200 * 1024; // 200 KB +// var fileName = FileHelper.GenerateRandomFileName(); +// var formFile = FileHelper.GenerateFormFile(fileName, size); +// +// // Act +// var result = await Storage.UploadToStorageAsync(formFile); +// var localFile = await Storage.DownloadAsync(result.Value.Name); +// +// // Assert +// result.IsSuccess.Should().BeTrue(); +// localFile.Value.FileInfo.Length.Should().Be(formFile.Length); +// localFile.Value.Name.Should().Be(fileName); +// +// await Storage.DeleteAsync(fileName); +// } +// +// [Fact] +// public async Task DownloadAsFileResult_WithFileName() +// { +// // Arrange +// const int size = 200 * 1024; // 200 KB +// var fileName = FileHelper.GenerateRandomFileName(); +// var localFile = FileHelper.GenerateLocalFile(fileName, size); +// +// // Act +// await Storage.UploadAsync(localFile.FileInfo); +// var result = await Storage.DownloadAsFileResult(fileName); +// +// // Assert +// result.IsSuccess.Should().BeTrue(); +// result.Value!.ContentType.Should().Be(MimeHelper.GetMimeType(localFile.FileInfo.Extension)); +// result.Value.FileDownloadName.Should().Be(localFile.Name); +// +// await Storage.DeleteAsync(fileName); +// } +// +// [Fact] +// public async Task DownloadAsFileResult_WithBlobMetadata() +// { +// // Arrange +// const int size = 200 * 1024; // 200 KB +// var fileName = FileHelper.GenerateRandomFileName(); +// var localFile = FileHelper.GenerateLocalFile(fileName, size); +// +// BlobMetadata blobMetadata = new() { Name = fileName }; +// +// // Act +// await Storage.UploadAsync(localFile.FileInfo, options => { options.FileName = fileName; }); +// var result = await Storage.DownloadAsFileResult(fileName); +// +// // Assert +// result.IsSuccess.Should().Be(true); +// result.Value!.ContentType.Should().Be(MimeHelper.GetMimeType(localFile.FileInfo.Extension)); +// result.Value.FileDownloadName.Should().Be(localFile.Name); +// +// await Storage.DeleteAsync(fileName); +// } +// +// [Fact] +// public async Task DownloadAsFileResult_WithFileName_IfFileDontExist() +// { +// // Arrange +// var fileName = FileHelper.GenerateRandomFileName(); +// +// // Act +// var fileResult = await Storage.DownloadAsFileResult(fileName); +// +// // Assert +// fileResult.IsSuccess.Should().BeFalse(); +// } +// +// [Fact] +// public async Task DownloadAsFileResult_WithBlobMetadata_IfFileDontExist() +// { +// // Arrange +// var fileName = FileHelper.GenerateRandomFileName(); +// +// BlobMetadata blobMetadata = new() { Name = fileName }; +// +// // Act +// var fileResult = await Storage.DownloadAsFileResult(blobMetadata); +// +// // Assert +// fileResult.IsSuccess.Should().BeFalse(); +// } +// } \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/FileSystem/FileSystemTests.cs b/ManagedCode.Storage.Tests/FileSystem/FileSystemTests.cs deleted file mode 100644 index 64897648..00000000 --- a/ManagedCode.Storage.Tests/FileSystem/FileSystemTests.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.IO; -using FluentAssertions; -using ManagedCode.Storage.Core; -using ManagedCode.Storage.FileSystem; -using ManagedCode.Storage.FileSystem.Extensions; -using ManagedCode.Storage.FileSystem.Options; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace ManagedCode.Storage.Tests.FileSystem; - -public class FileSystemTests : StorageBaseTests -{ - protected override ServiceProvider ConfigureServices() - { - var services = new ServiceCollection(); - - services.AddFileSystemStorageAsDefault(opt => { opt.BaseFolder = Path.Combine(Environment.CurrentDirectory, "managed-code-bucket"); }); - - services.AddFileSystemStorage(new FileSystemStorageOptions - { - BaseFolder = Path.Combine(Environment.CurrentDirectory, "managed-code-bucket") - }); - return services.BuildServiceProvider(); - } - - [Fact] - public void StorageAsDefaultTest() - { - var storage = ServiceProvider.GetService(); - var defaultStorage = ServiceProvider.GetService(); - storage?.GetType().FullName.Should().Be(defaultStorage?.GetType().FullName); - } -} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/ManagedCode.Storage.Tests.csproj b/ManagedCode.Storage.Tests/ManagedCode.Storage.Tests.csproj index f929631d..16ddee8c 100644 --- a/ManagedCode.Storage.Tests/ManagedCode.Storage.Tests.csproj +++ b/ManagedCode.Storage.Tests/ManagedCode.Storage.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 true false 10 @@ -18,18 +18,21 @@ - - + + - - + + - + - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -44,14 +47,15 @@ - + - + - + + diff --git a/ManagedCode.Storage.Tests/StorageBaseTests.cs b/ManagedCode.Storage.Tests/StorageBaseTests.cs deleted file mode 100644 index f6a3a73b..00000000 --- a/ManagedCode.Storage.Tests/StorageBaseTests.cs +++ /dev/null @@ -1,489 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FluentAssertions; -using ManagedCode.Storage.Core; -using ManagedCode.Storage.Core.Models; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -// ReSharper disable MethodHasAsyncOverload - -namespace ManagedCode.Storage.Tests; - -public abstract class StorageBaseTests -{ - protected StorageBaseTests() - { - ServiceProvider = ConfigureServices(); - Storage = ServiceProvider.GetService()!; - } - - protected IStorage Storage { get; } - protected ServiceProvider ServiceProvider { get; } - - protected abstract ServiceProvider ConfigureServices(); - - [Fact] - public async Task CreateContainer_ShouldBeSuccess() - { - var container = await Storage.CreateContainerAsync(); - container.IsSuccess.Should().BeTrue(); - } - - [Fact(Skip = "Other tests fail because container removal is too slow")] - public async Task RemoveContainer_ShouldBeSuccess() - { - var createResult = await Storage.CreateContainerAsync(); - createResult.IsSuccess.Should().BeTrue(); - - var result = await Storage.RemoveContainerAsync(); - - result.IsSuccess.Should().BeTrue(); - } - - [Fact] - public async Task StreamUploadAsyncTest() - { - var file = await GetTestFileAsync(); - var uploadResult = await Storage.UploadAsync(file.OpenRead()); - uploadResult.IsSuccess.Should().BeTrue(); - } - - [Fact] - public async Task ArrayUploadAsyncTest() - { - var file = await GetTestFileAsync(); - var bytes = await File.ReadAllBytesAsync(file.FullName); - var uploadResult = await Storage.UploadAsync(bytes); - uploadResult.IsSuccess.Should().BeTrue(); - } - - [Fact] - public async Task StringUploadAsyncTest() - { - var file = await GetTestFileAsync(); - var text = await File.ReadAllTextAsync(file.FullName); - var uploadResult = await Storage.UploadAsync(text); - uploadResult.IsSuccess.Should().BeTrue(); - } - - [Fact] - public async Task FileInfoUploadAsyncTest() - { - var file = await GetTestFileAsync(); - var uploadResult = await Storage.UploadAsync(file); - uploadResult.IsSuccess.Should().BeTrue(); - - var downloadResult = await Storage.DownloadAsync(uploadResult.Value!.Name); - downloadResult.IsSuccess.Should().BeTrue(); - } - - [Fact] - public async Task GetFileListAsyncTest() - { - await StreamUploadAsyncTest(); - await ArrayUploadAsyncTest(); - await StringUploadAsyncTest(); - - var files = await Storage.GetBlobMetadataListAsync().ToListAsync(); - files.Count.Should().BeGreaterOrEqualTo(3); - } - - [Fact] - public async Task DeleteDirectory_ShouldBeSuccess() - { - // Arrange - var directory = "test-directory"; - await UploadTestFileListAsync(directory, 3); - - // Act - var result = await Storage.DeleteDirectoryAsync(directory); - var blobs = await Storage.GetBlobMetadataListAsync(directory).ToListAsync(); - - // Assert - result.IsSuccess.Should().BeTrue(); - blobs.Count.Should().Be(0); - } - - #region Download - - [Fact] - public async Task DownloadAsync_WithoutOptions_AsLocalFile() - { - // Arrange - var fileInfo = await UploadTestFileAsync(); - - // Act - var result = await Storage.DownloadAsync(fileInfo.Name); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value!.FileInfo.Length.Should().Be(fileInfo.Length); - - await Storage.DeleteAsync(fileInfo.Name); - } - - #endregion - - #region CreateContainer - - [Fact] - public async Task CreateContainerAsync() - { - await FluentActions.Awaiting(() => Storage.CreateContainerAsync()) - .Should() - .NotThrowAsync(); - } - - #endregion - - private async Task UploadTestFileAsync(string? directory = null) - { - var file = await GetTestFileAsync(); - - UploadOptions options = new() { FileName = file.Name, Directory = directory }; - var result = await Storage.UploadAsync(file.OpenRead(), options); - result.IsSuccess.Should().BeTrue(); - - return file; - } - - private async Task> UploadTestFileListAsync(string? directory = null, int? count = 10) - { - List fileList = new(); - - for (var i = 0; i < count; i++) - { - var file = await UploadTestFileAsync(directory); - fileList.Add(file); - } - - return fileList; - } - - protected async Task GetTestFileAsync() - { - var fileName = Path.GetTempFileName(); - var fs = File.OpenWrite(fileName); - var sw = new StreamWriter(fs); - - for (var i = 0; i < 1000; i++) - { - await sw.WriteLineAsync(Guid.NewGuid().ToString()); - } - - await sw.DisposeAsync(); - await fs.DisposeAsync(); - - return new FileInfo(fileName); - } - - #region MemoryPayload - -// [Fact] -// public async Task UploadBigFilesAsync() -// { -// const int fileSize = 70 * 1024 * 1024; -// -// var bigFiles = new List() -// { -// GetLocalFile(fileSize), -// GetLocalFile(fileSize), -// GetLocalFile(fileSize) -// }; -// -// foreach (var localFile in bigFiles) -// { -// await Storage.UploadStreamAsync(localFile.Name, localFile.FileStream); -// await localFile.DisposeAsync(); -// } -// -// Process currentProcess = Process.GetCurrentProcess(); -// long totalBytesOfMemoryUsed = currentProcess.WorkingSet64; -// -// totalBytesOfMemoryUsed.Should().BeLessThan(3 * fileSize); -// -// foreach (var localFile in bigFiles) -// { -// await Storage.DeleteAsync(localFile.Name); -// } -// } - - #endregion - - #region Get - - [Fact] - public async Task GetBlobListAsync_WithoutOptions() - { - // Arrange - var fileList = await UploadTestFileListAsync(); - - // Act - var result = await Storage.GetBlobMetadataListAsync().ToListAsync(); - - // Assert - result.Count.Should().BeGreaterThan(fileList.Count); - - foreach (var item in fileList) - { - var file = result.FirstOrDefault(f => f.Name == item.Name); - file.Should().NotBeNull(); - - await Storage.DeleteAsync(item.Name); - } - } - - [Fact] - public virtual async Task GetBlobMetadataAsync_ShouldBeTrue() - { - // Arrange - var fileInfo = await UploadTestFileAsync(); - - // Act - var result = await Storage.GetBlobMetadataAsync(fileInfo.Name); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value!.Length.Should().Be(fileInfo.Length); - result.Value!.Name.Should().Be(fileInfo.Name); - - await Storage.DeleteAsync(fileInfo.Name); - } - - #endregion - - #region Upload - - [Fact] - public async Task UploadAsync_AsText_WithoutOptions() - { - // Arrange - var uploadContent = FileHelper.GenerateRandomFileContent(); - - // Act - var result = await Storage.UploadAsync(uploadContent); - var downloadedResult = await Storage.DownloadAsync(result.Value!.Name); - - // Assert - result.IsSuccess.Should().BeTrue(); - downloadedResult.IsSuccess.Should().BeTrue(); - } - - [Fact] - public async Task UploadAsync_AsStream_WithoutOptions() - { - // Arrange - var uploadContent = FileHelper.GenerateRandomFileContent(); - var byteArray = Encoding.ASCII.GetBytes(uploadContent); - var stream = new MemoryStream(byteArray); - - // Act - var result = await Storage.UploadAsync(stream); - var downloadedResult = await Storage.DownloadAsync(result.Value!.Name); - - // Assert - result.IsSuccess.Should().BeTrue(); - downloadedResult.IsSuccess.Should().BeTrue(); - } - - #region Directory - - [Fact] - public async Task UploadAsync_AsStream_WithOptions_ToDirectory_SpecifyingFileName() - { - // Arrange - var directory = "test-directory"; - var uploadContent = FileHelper.GenerateRandomFileContent(); - var fileName = FileHelper.GenerateRandomFileName(); - - var byteArray = Encoding.ASCII.GetBytes(uploadContent); - var stream = new MemoryStream(byteArray); - - // Act - var result = await Storage.UploadAsync(stream, new UploadOptions { FileName = fileName, Directory = directory }); - var downloadedResult = await Storage.DownloadAsync(new DownloadOptions { FileName = fileName, Directory = directory }); - - // Assert - result.IsSuccess.Should().BeTrue(); - downloadedResult.IsSuccess.Should().BeTrue(); - } - - [Fact] - public async Task UploadAsync_AsArray_WithOptions_ToDirectory_SpecifyingFileName() - { - // Arrange - var directory = "test-directory"; - var uploadContent = FileHelper.GenerateRandomFileContent(); - var fileName = FileHelper.GenerateRandomFileName(); - - var byteArray = Encoding.ASCII.GetBytes(uploadContent); - - // Act - var result = await Storage.UploadAsync(byteArray, new UploadOptions { FileName = fileName, Directory = directory }); - var downloadedResult = await Storage.DownloadAsync(new DownloadOptions { FileName = fileName, Directory = directory }); - - // Assert - result.IsSuccess.Should().BeTrue(); - downloadedResult.IsSuccess.Should().BeTrue(); - - await Storage.DeleteAsync(fileName); - } - - [Fact] - public async Task UploadAsync_AsText_WithOptions_ToDirectory_SpecifyingFileName() - { - // Arrange - var directory = "test-directory"; - var uploadContent = FileHelper.GenerateRandomFileContent(); - var fileName = FileHelper.GenerateRandomFileName(); - - // Act - var result = await Storage.UploadAsync(uploadContent, new UploadOptions { FileName = fileName, Directory = directory }); - var downloadedResult = await Storage.DownloadAsync(new DownloadOptions { FileName = fileName, Directory = directory }); - - // Assert - result.IsSuccess.Should().BeTrue(); - downloadedResult.IsSuccess.Should().BeTrue(); - - await Storage.DeleteAsync(fileName); - } - - #endregion - - #endregion - - #region Exist - - [Fact] - public async Task ExistsAsync_WithoutOptions_ShouldBeTrue() - { - // Arrange - var fileInfo = await UploadTestFileAsync(); - - // Act - var result = await Storage.ExistsAsync(fileInfo.Name); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().BeTrue(); - - await Storage.DeleteAsync(fileInfo.Name); - } - - [Fact] - public async Task ExistsAsync_WithOptions_InDirectory_ShouldBeTrue() - { - // Arrange - var directory = "test-directory"; - var fileInfo = await UploadTestFileAsync(directory); - ExistOptions options = new() { FileName = fileInfo.Name, Directory = directory }; - - // Act - var result = await Storage.ExistsAsync(options); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().BeTrue(); - - await Storage.DeleteAsync(fileInfo.Name); - } - - [Fact] - public async Task ExistsAsync_IfFileDontExist_WithoutOptions_ShouldBeFalse() - { - // Act - var result = await Storage.ExistsAsync(Guid.NewGuid().ToString()); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().BeFalse(); - } - - [Fact] - public async Task ExistsAsync_IfFileFileExistInAnotherDirectory_WithOptions_ShouldBeFalse() - { - // Arrange - var directory = "test-directory"; - var fileInfo = await UploadTestFileAsync(directory); - ExistOptions options = new() { FileName = fileInfo.Name, Directory = "another-directory" }; - - // Act - var result = await Storage.ExistsAsync(options); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().BeFalse(); - - await Storage.DeleteAsync(fileInfo.Name); - } - - #endregion - - #region Delete - - [Fact] - public async Task DeleteAsync_WithoutOptions_ShouldTrue() - { - // Arrange - var file = await UploadTestFileAsync(); - - // Act - var result = await Storage.DeleteAsync(file.Name); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().BeTrue(); - } - - [Fact] - public async Task DeleteAsync_WithoutOptions_IfFileDontExist_ShouldFalse() - { - // Arrange - var blob = Guid.NewGuid().ToString(); - - // Act - var result = await Storage.DeleteAsync(blob); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().BeFalse(); - } - - [Fact] - public async Task DeleteAsync_WithOptions_FromDirectory_ShouldTrue() - { - // Arrange - var directory = "test-directory"; - var file = await UploadTestFileAsync(directory); - DeleteOptions options = new() { FileName = file.Name, Directory = directory }; - - // Act - var result = await Storage.DeleteAsync(options); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().BeTrue(); - } - - [Fact] - public async Task DeleteAsync_WithOptions_IfFileDontExist_FromDirectory_ShouldFalse() - { - // Arrange - var directory = "test-directory"; - DeleteOptions options = new() { FileName = Guid.NewGuid().ToString(), Directory = directory }; - - // Act - var result = await Storage.DeleteAsync(options); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().BeFalse(); - } - - #endregion -} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/AWS/AWSBlobTests.cs b/ManagedCode.Storage.Tests/Storages/AWS/AWSBlobTests.cs new file mode 100644 index 00000000..ba9183d2 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/AWS/AWSBlobTests.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using Testcontainers.LocalStack; + +namespace ManagedCode.Storage.Tests.Storages.AWS; + +public class AWSBlobTests : BlobTests +{ + protected override LocalStackContainer Build() + { + return new LocalStackBuilder().Build(); + } + + protected override ServiceProvider ConfigureServices() + { + return AWSConfigurator.ConfigureServices(Container.GetConnectionString()); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/AWS/AWSConfigurator.cs b/ManagedCode.Storage.Tests/Storages/AWS/AWSConfigurator.cs new file mode 100644 index 00000000..cff66923 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/AWS/AWSConfigurator.cs @@ -0,0 +1,37 @@ +using Amazon.S3; +using ManagedCode.Storage.Aws.Extensions; +using ManagedCode.Storage.Aws.Options; +using Microsoft.Extensions.DependencyInjection; + +// ReSharper disable MethodHasAsyncOverload + +namespace ManagedCode.Storage.Tests.Storages.AWS; + +public class AWSConfigurator +{ + public static ServiceProvider ConfigureServices(string connectionString) + { + + var services = new ServiceCollection(); + + var config = new AmazonS3Config(); + config.ServiceURL = connectionString; + + services.AddAWSStorageAsDefault(opt => + { + opt.PublicKey = "localkey"; + opt.SecretKey = "localsecret"; + opt.Bucket = "managed-code-bucket"; + opt.OriginalOptions = config; + }); + + services.AddAWSStorage(new AWSStorageOptions + { + PublicKey = "localkey", + SecretKey = "localsecret", + Bucket = "managed-code-bucket", + OriginalOptions = config + }); + return services.BuildServiceProvider(); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/AWS/AWSContainerTests.cs b/ManagedCode.Storage.Tests/Storages/AWS/AWSContainerTests.cs new file mode 100644 index 00000000..535c261f --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/AWS/AWSContainerTests.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using Testcontainers.LocalStack; + +namespace ManagedCode.Storage.Tests.Storages.AWS; + +public class AWSContainerTests : ContainerTests +{ + protected override LocalStackContainer Build() + { + return new LocalStackBuilder().Build(); + } + + protected override ServiceProvider ConfigureServices() + { + return AWSConfigurator.ConfigureServices(Container.GetConnectionString()); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/AWS/AWSDownloadTests.cs b/ManagedCode.Storage.Tests/Storages/AWS/AWSDownloadTests.cs new file mode 100644 index 00000000..b2782e69 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/AWS/AWSDownloadTests.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using Testcontainers.LocalStack; + +namespace ManagedCode.Storage.Tests.Storages.AWS; + +public class AWSDownloadTests : DownloadTests +{ + protected override LocalStackContainer Build() + { + return new LocalStackBuilder().Build(); + } + + protected override ServiceProvider ConfigureServices() + { + return AWSConfigurator.ConfigureServices(Container.GetConnectionString()); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/AWS/AWSUploadTests.cs b/ManagedCode.Storage.Tests/Storages/AWS/AWSUploadTests.cs new file mode 100644 index 00000000..a7f15969 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/AWS/AWSUploadTests.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using Testcontainers.LocalStack; + +namespace ManagedCode.Storage.Tests.Storages.AWS; + +public class AWSUploadTests : UploadTests +{ + protected override LocalStackContainer Build() + { + return new LocalStackBuilder().Build(); + } + + protected override ServiceProvider ConfigureServices() + { + return AWSConfigurator.ConfigureServices(Container.GetConnectionString()); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/AWS/AWSStorageTests.cs b/ManagedCode.Storage.Tests/Storages/AWS/AwsConfigTests.cs similarity index 55% rename from ManagedCode.Storage.Tests/AWS/AWSStorageTests.cs rename to ManagedCode.Storage.Tests/Storages/AWS/AwsConfigTests.cs index 064701b9..0f60a8e3 100644 --- a/ManagedCode.Storage.Tests/AWS/AWSStorageTests.cs +++ b/ManagedCode.Storage.Tests/Storages/AWS/AwsConfigTests.cs @@ -1,6 +1,4 @@ using System; -using Amazon; -using Amazon.S3; using FluentAssertions; using ManagedCode.Storage.Aws; using ManagedCode.Storage.Aws.Extensions; @@ -10,41 +8,13 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace ManagedCode.Storage.Tests.AWS; +namespace ManagedCode.Storage.Tests.Storages.AWS; -public class AWSStorageTests : StorageBaseTests -{ - protected override ServiceProvider ConfigureServices() - { - var services = new ServiceCollection(); - - // AWS library overwrites property values. you should only create configurations this way. - var awsConfig = new AmazonS3Config - { - RegionEndpoint = RegionEndpoint.EUWest1, - ForcePathStyle = true, - UseHttp = true, - ServiceURL = "http://localhost:4566" // this is the default port for the aws s3 emulator, must be last in the list - }; - services.AddAWSStorageAsDefault(opt => - { - opt.PublicKey = "localkey"; - opt.SecretKey = "localsecret"; - opt.Bucket = "managed-code-bucket"; - opt.OriginalOptions = awsConfig; - }); - - services.AddAWSStorage(new AWSStorageOptions - { - PublicKey = "localkey", - SecretKey = "localsecret", - Bucket = "managed-code-bucket", - OriginalOptions = awsConfig - }); - return services.BuildServiceProvider(); - } +public class AwsConfigTests +{ + [Fact] public void BadConfigurationForStorage_WithoutPublicKey_ThrowException() { @@ -90,8 +60,10 @@ public void BadConfigurationForStorage_WithoutBucket_ThrowException() [Fact] public void StorageAsDefaultTest() { - var storage = ServiceProvider.GetService(); - var defaultStorage = ServiceProvider.GetService(); + var storage = AWSConfigurator.ConfigureServices("http://localhost").GetService(); + var defaultStorage = AWSConfigurator.ConfigureServices("http://localhost").GetService(); storage?.GetType().FullName.Should().Be(defaultStorage?.GetType().FullName); } -} \ No newline at end of file + +} + diff --git a/ManagedCode.Storage.Tests/Storages/Azure/AzureBlobTests.cs b/ManagedCode.Storage.Tests/Storages/Azure/AzureBlobTests.cs new file mode 100644 index 00000000..9a597755 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/Azure/AzureBlobTests.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using Testcontainers.Azurite; + +namespace ManagedCode.Storage.Tests.Storages.Azure; + +public class AzureBlobTests : BlobTests +{ + protected override AzuriteContainer Build() + { + return new AzuriteBuilder() + .WithImage("mcr.microsoft.com/azure-storage/azurite:3.26.0") + .Build(); + } + + protected override ServiceProvider ConfigureServices() + { + return AzureConfigurator.ConfigureServices(Container.GetConnectionString()); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/Azure/AzureConfigTests.cs b/ManagedCode.Storage.Tests/Storages/Azure/AzureConfigTests.cs new file mode 100644 index 00000000..dd531358 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/Azure/AzureConfigTests.cs @@ -0,0 +1,53 @@ +using System; +using FluentAssertions; +using ManagedCode.Storage.Azure; +using ManagedCode.Storage.Azure.Extensions; +using ManagedCode.Storage.Core; +using ManagedCode.Storage.Core.Exceptions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace ManagedCode.Storage.Tests.Storages.Azure; + + +public class AzureConfigTests +{ + + + [Fact] + public void BadConfigurationForStorage_WithoutContainer_ThrowException() + { + var services = new ServiceCollection(); + + Action action = () => services.AddAzureStorage(opt => + { + opt.ConnectionString = "test"; + }); + + action.Should().Throw(); + } + + [Fact] + public void BadConfigurationForStorage_WithoutConnectionString_ThrowException() + { + var services = new ServiceCollection(); + + Action action = () => + services.AddAzureStorageAsDefault(options => + { + options.Container = "managed-code-bucket"; + options.ConnectionString = null; + }); + + action.Should().Throw(); + } + + [Fact] + public void StorageAsDefaultTest() + { + var connectionString = "UseDevelopmentStorage=true"; + var storage = AzureConfigurator.ConfigureServices(connectionString).GetService(); + var defaultStorage = AzureConfigurator.ConfigureServices(connectionString).GetService(); + storage?.GetType().FullName.Should().Be(defaultStorage?.GetType().FullName); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/Azure/AzureConfigurator.cs b/ManagedCode.Storage.Tests/Storages/Azure/AzureConfigurator.cs new file mode 100644 index 00000000..d2c2eb97 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/Azure/AzureConfigurator.cs @@ -0,0 +1,29 @@ +using ManagedCode.Storage.Azure.Extensions; +using ManagedCode.Storage.Azure.Options; +using Microsoft.Extensions.DependencyInjection; + +// ReSharper disable MethodHasAsyncOverload + +namespace ManagedCode.Storage.Tests.Storages.Azure; + +public class AzureConfigurator +{ + public static ServiceProvider ConfigureServices(string connectionString) + { + + var services = new ServiceCollection(); + + services.AddAzureStorageAsDefault(opt => + { + opt.Container = "managed-code-bucket"; + opt.ConnectionString = connectionString; + }); + + services.AddAzureStorage(new AzureStorageOptions + { + Container = "managed-code-bucket", + ConnectionString = connectionString + }); + return services.BuildServiceProvider(); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/Azure/AzureContainerTests.cs b/ManagedCode.Storage.Tests/Storages/Azure/AzureContainerTests.cs new file mode 100644 index 00000000..acea422a --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/Azure/AzureContainerTests.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using Testcontainers.Azurite; + +namespace ManagedCode.Storage.Tests.Storages.Azure; + +public class AzureContainerTests : ContainerTests +{ + protected override AzuriteContainer Build() + { + return new AzuriteBuilder() + .WithImage("mcr.microsoft.com/azure-storage/azurite:3.26.0") + .Build(); + } + + protected override ServiceProvider ConfigureServices() + { + return AzureConfigurator.ConfigureServices(Container.GetConnectionString()); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Azure/AzureDataLakeTests.cs b/ManagedCode.Storage.Tests/Storages/Azure/AzureDataLakeTests.cs similarity index 90% rename from ManagedCode.Storage.Tests/Azure/AzureDataLakeTests.cs rename to ManagedCode.Storage.Tests/Storages/Azure/AzureDataLakeTests.cs index 926cfff9..728edadc 100644 --- a/ManagedCode.Storage.Tests/Azure/AzureDataLakeTests.cs +++ b/ManagedCode.Storage.Tests/Storages/Azure/AzureDataLakeTests.cs @@ -1,4 +1,4 @@ -// using ManagedCode.Storage.AzureDataLake.Extensions; +// using ManagedCode.Storage.Azure.DataLake.Extensions; // using Microsoft.Extensions.DependencyInjection; // // namespace ManagedCode.Storage.Tests.Azure; diff --git a/ManagedCode.Storage.Tests/Storages/Azure/AzureDownloadTests.cs b/ManagedCode.Storage.Tests/Storages/Azure/AzureDownloadTests.cs new file mode 100644 index 00000000..f72cc4eb --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/Azure/AzureDownloadTests.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using Testcontainers.Azurite; + +namespace ManagedCode.Storage.Tests.Storages.Azure; + +public class AzureDownloadTests : DownloadTests +{ + protected override AzuriteContainer Build() + { + return new AzuriteBuilder() + .WithImage("mcr.microsoft.com/azure-storage/azurite:3.26.0") + .Build(); + } + + protected override ServiceProvider ConfigureServices() + { + return AzureConfigurator.ConfigureServices(Container.GetConnectionString()); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/Azure/AzureUploadTests.cs b/ManagedCode.Storage.Tests/Storages/Azure/AzureUploadTests.cs new file mode 100644 index 00000000..e6e685d5 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/Azure/AzureUploadTests.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using Testcontainers.Azurite; + +namespace ManagedCode.Storage.Tests.Storages.Azure; + +public class AzureUploadTests : UploadTests +{ + protected override AzuriteContainer Build() + { + return new AzuriteBuilder() + .WithImage("mcr.microsoft.com/azure-storage/azurite:3.26.0") + .Build(); + } + + protected override ServiceProvider ConfigureServices() + { + return AzureConfigurator.ConfigureServices(Container.GetConnectionString()); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/BlobTests.cs b/ManagedCode.Storage.Tests/Storages/BlobTests.cs new file mode 100644 index 00000000..f8e811af --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/BlobTests.cs @@ -0,0 +1,173 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using DotNet.Testcontainers.Containers; +using FluentAssertions; +using ManagedCode.Storage.Core.Models; +using ManagedCode.Storage.Tests.Common; +using Xunit; + +namespace ManagedCode.Storage.Tests.Storages; + +public abstract class BlobTests : BaseContainer where T : IContainer +{ + [Fact] + public async Task GetBlobListAsync_WithoutOptions() + { + // Arrange + var fileList = await UploadTestFileListAsync(); + + // Act + var result = await Storage.GetBlobMetadataListAsync().ToListAsync(); + + // Assert + result.Count.Should().Be(fileList.Count); + + foreach (var item in fileList) + { + var file = result.FirstOrDefault(f => f.Name == item.Name); + file.Should().NotBeNull(); + + await Storage.DeleteAsync(item.Name); + } + } + + [Fact] + public virtual async Task GetBlobMetadataAsync_ShouldBeTrue() + { + // Arrange + var fileInfo = await UploadTestFileAsync(); + + // Act + var result = await Storage.GetBlobMetadataAsync(fileInfo.Name); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value!.Length.Should().Be(fileInfo.Length); + result.Value!.Name.Should().Be(fileInfo.Name); + + await Storage.DeleteAsync(fileInfo.Name); + } + + [Fact] + public async Task DeleteAsync_WithoutOptions_ShouldTrue() + { + // Arrange + var file = await UploadTestFileAsync(); + + // Act + var result = await Storage.DeleteAsync(file.Name); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeTrue(); + } + + [Fact] + public async Task DeleteAsync_WithoutOptions_IfFileDontExist_ShouldFalse() + { + // Arrange + var blob = Guid.NewGuid().ToString(); + + // Act + var result = await Storage.DeleteAsync(blob); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeFalse(); + } + + [Fact] + public async Task DeleteAsync_WithOptions_FromDirectory_ShouldTrue() + { + // Arrange + var directory = "test-directory"; + var file = await UploadTestFileAsync(directory); + DeleteOptions options = new() { FileName = file.Name, Directory = directory }; + + // Act + var result = await Storage.DeleteAsync(options); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeTrue(); + } + + [Fact] + public async Task DeleteAsync_WithOptions_IfFileDontExist_FromDirectory_ShouldFalse() + { + // Arrange + var directory = "test-directory"; + DeleteOptions options = new() { FileName = Guid.NewGuid().ToString(), Directory = directory }; + + // Act + var result = await Storage.DeleteAsync(options); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeFalse(); + } + + [Fact] + public async Task ExistsAsync_WithoutOptions_ShouldBeTrue() + { + // Arrange + var fileInfo = await UploadTestFileAsync(); + + // Act + var result = await Storage.ExistsAsync(fileInfo.Name); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeTrue(); + + await Storage.DeleteAsync(fileInfo.Name); + } + + [Fact] + public async Task ExistsAsync_WithOptions_InDirectory_ShouldBeTrue() + { + // Arrange + var directory = "test-directory"; + var fileInfo = await UploadTestFileAsync(directory); + ExistOptions options = new() { FileName = fileInfo.Name, Directory = directory }; + + // Act + var result = await Storage.ExistsAsync(options); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeTrue(); + + await Storage.DeleteAsync(fileInfo.Name); + } + + [Fact] + public async Task ExistsAsync_IfFileDontExist_WithoutOptions_ShouldBeFalse() + { + // Act + var result = await Storage.ExistsAsync(Guid.NewGuid().ToString()); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeFalse(); + } + + [Fact] + public async Task ExistsAsync_IfFileFileExistInAnotherDirectory_WithOptions_ShouldBeFalse() + { + // Arrange + var directory = "test-directory"; + var fileInfo = await UploadTestFileAsync(directory); + ExistOptions options = new() { FileName = fileInfo.Name, Directory = "another-directory" }; + + // Act + var result = await Storage.ExistsAsync(options); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeFalse(); + + await Storage.DeleteAsync(fileInfo.Name); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/ContainerTests.cs b/ManagedCode.Storage.Tests/Storages/ContainerTests.cs new file mode 100644 index 00000000..a47257d5 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/ContainerTests.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using DotNet.Testcontainers.Containers; +using FluentAssertions; +using ManagedCode.Storage.Tests.Common; +using Xunit; + +namespace ManagedCode.Storage.Tests.Storages; + +public abstract class ContainerTests : BaseContainer where T : IContainer +{ + + [Fact] + public async Task CreateContainer_ShouldBeSuccess() + { + var container = await Storage.CreateContainerAsync(); + container.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task CreateContainerAsync() + { + await FluentActions.Awaiting(() => Storage.CreateContainerAsync()) + .Should() + .NotThrowAsync(); + } + + [Fact] + public async Task RemoveContainer_ShouldBeSuccess() + { + var createResult = await Storage.CreateContainerAsync(); + createResult.IsSuccess.Should().BeTrue(); + + var result = await Storage.RemoveContainerAsync(); + + result.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task GetFileListAsyncTest() + { + await UploadTestFileAsync(); + await UploadTestFileAsync(); + await UploadTestFileAsync(); + + var files = await Storage.GetBlobMetadataListAsync().ToListAsync(); + files.Count.Should().BeGreaterOrEqualTo(3); + } + + [Fact] + public async Task DeleteDirectory_ShouldBeSuccess() + { + // Arrange + var directory = "test-directory"; + await UploadTestFileListAsync(directory, 3); + + // Act + var result = await Storage.DeleteDirectoryAsync(directory); + var blobs = await Storage.GetBlobMetadataListAsync(directory).ToListAsync(); + + // Assert + result.IsSuccess.Should().BeTrue(); + blobs.Count.Should().Be(0); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/DownloadTests.cs b/ManagedCode.Storage.Tests/Storages/DownloadTests.cs new file mode 100644 index 00000000..d4158715 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/DownloadTests.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using DotNet.Testcontainers.Containers; +using FluentAssertions; +using ManagedCode.Storage.Tests.Common; +using Xunit; + +namespace ManagedCode.Storage.Tests.Storages; + +public abstract class DownloadTests : BaseContainer where T : IContainer +{ + [Fact] + public async Task DownloadAsync_WithoutOptions_AsLocalFile() + { + // Arrange + var fileInfo = await UploadTestFileAsync(); + + // Act + var result = await Storage.DownloadAsync(fileInfo.Name); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value!.FileInfo.Length.Should().Be(fileInfo.Length); + + await Storage.DeleteAsync(fileInfo.Name); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemBlobTests.cs b/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemBlobTests.cs new file mode 100644 index 00000000..7073dead --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemBlobTests.cs @@ -0,0 +1,20 @@ +using ManagedCode.Storage.Tests.Common; +using ManagedCode.Storage.Tests.Storages.GCS; +using Microsoft.Extensions.DependencyInjection; + +// ReSharper disable MethodHasAsyncOverload + +namespace ManagedCode.Storage.Tests.Storages.FileSystem; + +public class FileSystemBlobTests : BlobTests +{ + protected override EmptyContainer Build() + { + return new EmptyContainer(); + } + + protected override ServiceProvider ConfigureServices() + { + return FileSystemConfigurator.ConfigureServices("managed-code-blob"); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemConfigurator.cs b/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemConfigurator.cs new file mode 100644 index 00000000..4c9b7e6f --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemConfigurator.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using ManagedCode.Storage.FileSystem.Extensions; +using ManagedCode.Storage.FileSystem.Options; +using Microsoft.Extensions.DependencyInjection; + +namespace ManagedCode.Storage.Tests.Storages.FileSystem; + +public class FileSystemConfigurator +{ + public static ServiceProvider ConfigureServices(string connectionString) + { + connectionString += Random.Shared.NextInt64(); + var services = new ServiceCollection(); + + services.AddFileSystemStorageAsDefault(opt => { opt.BaseFolder = Path.Combine(Environment.CurrentDirectory,connectionString); }); + services.AddFileSystemStorage(new FileSystemStorageOptions + { + BaseFolder = Path.Combine(Environment.CurrentDirectory, connectionString) + }); + return services.BuildServiceProvider(); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemContainerTests.cs b/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemContainerTests.cs new file mode 100644 index 00000000..02d52533 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemContainerTests.cs @@ -0,0 +1,18 @@ +using ManagedCode.Storage.Tests.Common; +using ManagedCode.Storage.Tests.Storages.GCS; +using Microsoft.Extensions.DependencyInjection; + +namespace ManagedCode.Storage.Tests.Storages.FileSystem; + +public class FileSystemContainerTests : ContainerTests +{ + protected override EmptyContainer Build() + { + return new EmptyContainer(); + } + + protected override ServiceProvider ConfigureServices() + { + return FileSystemConfigurator.ConfigureServices("managed-code-blob"); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemDownloadTests.cs b/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemDownloadTests.cs new file mode 100644 index 00000000..3fd9ca9e --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemDownloadTests.cs @@ -0,0 +1,18 @@ +using ManagedCode.Storage.Tests.Common; +using ManagedCode.Storage.Tests.Storages.GCS; +using Microsoft.Extensions.DependencyInjection; + +namespace ManagedCode.Storage.Tests.Storages.FileSystem; + +public class FileSystemDownloadTests : DownloadTests +{ + protected override EmptyContainer Build() + { + return new EmptyContainer(); + } + + protected override ServiceProvider ConfigureServices() + { + return FileSystemConfigurator.ConfigureServices("managed-code-blob"); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemTests.cs b/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemTests.cs new file mode 100644 index 00000000..9e5025d3 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemTests.cs @@ -0,0 +1,19 @@ +using FluentAssertions; +using ManagedCode.Storage.Core; +using ManagedCode.Storage.FileSystem; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace ManagedCode.Storage.Tests.Storages.FileSystem; + +public class FileSystemTests +{ + + [Fact] + public void StorageAsDefaultTest() + { + var storage = FileSystemConfigurator.ConfigureServices("test").GetService(); + var defaultStorage = FileSystemConfigurator.ConfigureServices("test").GetService(); + storage?.GetType().FullName.Should().Be(defaultStorage?.GetType().FullName); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemUploadTests.cs b/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemUploadTests.cs new file mode 100644 index 00000000..39eabcfa --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemUploadTests.cs @@ -0,0 +1,18 @@ +using ManagedCode.Storage.Tests.Common; +using ManagedCode.Storage.Tests.Storages.GCS; +using Microsoft.Extensions.DependencyInjection; + +namespace ManagedCode.Storage.Tests.Storages.FileSystem; + +public class FileSystemUploadTests : UploadTests +{ + protected override EmptyContainer Build() + { + return new EmptyContainer(); + } + + protected override ServiceProvider ConfigureServices() + { + return FileSystemConfigurator.ConfigureServices("managed-code-blob"); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/GCS/GCSBlobTests.cs b/ManagedCode.Storage.Tests/Storages/GCS/GCSBlobTests.cs new file mode 100644 index 00000000..ad8a87d1 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/GCS/GCSBlobTests.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using TestcontainersGCS; +using Xunit; + +// ReSharper disable MethodHasAsyncOverload + +namespace ManagedCode.Storage.Tests.Storages.GCS; + +[Collection("Google")] +public class GCSBlobTests : BlobTests +{ + protected override GCSContainer Build() + { + return new GCSBuilder().Build(); + } + + protected override ServiceProvider ConfigureServices() + { + return GCSConfigurator.ConfigureServices(Container.GetConnectionString()); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/GCP/GoogleStorageTests.cs b/ManagedCode.Storage.Tests/Storages/GCS/GCSConfigTests.cs similarity index 58% rename from ManagedCode.Storage.Tests/GCP/GoogleStorageTests.cs rename to ManagedCode.Storage.Tests/Storages/GCS/GCSConfigTests.cs index 88693656..2a0f464d 100644 --- a/ManagedCode.Storage.Tests/GCP/GoogleStorageTests.cs +++ b/ManagedCode.Storage.Tests/Storages/GCS/GCSConfigTests.cs @@ -1,54 +1,19 @@ -using System; +using System; using FluentAssertions; using Google.Cloud.Storage.V1; using ManagedCode.Storage.Core; using ManagedCode.Storage.Core.Exceptions; -using ManagedCode.Storage.Gcp; -using ManagedCode.Storage.Gcp.Extensions; -using ManagedCode.Storage.Gcp.Options; +using ManagedCode.Storage.Google; +using ManagedCode.Storage.Google.Extensions; +using ManagedCode.Storage.Google.Options; using Microsoft.Extensions.DependencyInjection; using Xunit; -// ReSharper disable MethodHasAsyncOverload +namespace ManagedCode.Storage.Tests.Storages.GCS; -namespace ManagedCode.Storage.Tests.GCP; - -public class GoogleStorageTests : StorageBaseTests +[Collection("Google")] +public class GCSConfigTests { - protected override ServiceProvider ConfigureServices() - { - var services = new ServiceCollection(); - - services.AddGCPStorageAsDefault(opt => - { - opt.BucketOptions = new BucketOptions - { - ProjectId = "api-project-0000000000000", - Bucket = "managed-code-bucket" - }; - opt.StorageClientBuilder = new StorageClientBuilder - { - UnauthenticatedAccess = true, - BaseUri = "http://localhost:4443/storage/v1/" - }; - }); - - services.AddGCPStorage(new GCPStorageOptions - { - BucketOptions = new BucketOptions - { - ProjectId = "api-project-0000000000000", - Bucket = "managed-code-bucket" - }, - StorageClientBuilder = new StorageClientBuilder - { - UnauthenticatedAccess = true, - BaseUri = "http://localhost:4443/storage/v1/" - } - }); - return services.BuildServiceProvider(); - } - [Fact] public void BadConfigurationForStorage_WithoutProjectId_ThrowException() { @@ -111,8 +76,9 @@ public void BadConfigurationForStorage_WithoutStorageClientBuilderAndGoogleCrede [Fact] public void StorageAsDefaultTest() { - var storage = ServiceProvider.GetService(); - var defaultStorage = ServiceProvider.GetService(); + var storage = GCSConfigurator.ConfigureServices("test").GetService(); + var defaultStorage = GCSConfigurator.ConfigureServices("test").GetService(); storage?.GetType().FullName.Should().Be(defaultStorage?.GetType().FullName); } + } \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/GCS/GCSConfigurator.cs b/ManagedCode.Storage.Tests/Storages/GCS/GCSConfigurator.cs new file mode 100644 index 00000000..531a161a --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/GCS/GCSConfigurator.cs @@ -0,0 +1,46 @@ +using Google.Cloud.Storage.V1; +using ManagedCode.Storage.Google.Extensions; +using ManagedCode.Storage.Google.Options; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace ManagedCode.Storage.Tests.Storages.GCS; + +[Collection("Google")] +public class GCSConfigurator +{ + public static ServiceProvider ConfigureServices(string connectionString) + { + + var services = new ServiceCollection(); + + services.AddGCPStorageAsDefault(opt => + { + opt.BucketOptions = new BucketOptions + { + ProjectId = "api-project-0000000000000", + Bucket = "managed-code-bucket" + }; + opt.StorageClientBuilder = new StorageClientBuilder + { + UnauthenticatedAccess = true, + BaseUri = connectionString + }; + }); + + services.AddGCPStorage(new GCPStorageOptions + { + BucketOptions = new BucketOptions + { + ProjectId = "api-project-0000000000000", + Bucket = "managed-code-bucket" + }, + StorageClientBuilder = new StorageClientBuilder + { + UnauthenticatedAccess = true, + BaseUri = connectionString + } + }); + return services.BuildServiceProvider(); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/GCS/GCSContainerTests.cs b/ManagedCode.Storage.Tests/Storages/GCS/GCSContainerTests.cs new file mode 100644 index 00000000..4163fa8c --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/GCS/GCSContainerTests.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using TestcontainersGCS; +using Xunit; + +namespace ManagedCode.Storage.Tests.Storages.GCS; + +[Collection("Google")] +public class GCSContainerTests : ContainerTests +{ + protected override GCSContainer Build() + { + return new GCSBuilder().Build(); + } + + protected override ServiceProvider ConfigureServices() + { + return GCSConfigurator.ConfigureServices(Container.GetConnectionString()); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/GCS/GCSDownloadTests.cs b/ManagedCode.Storage.Tests/Storages/GCS/GCSDownloadTests.cs new file mode 100644 index 00000000..2b14cce1 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/GCS/GCSDownloadTests.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using TestcontainersGCS; +using Xunit; + +namespace ManagedCode.Storage.Tests.Storages.GCS; + +[Collection("Google")] +public class GCSDownloadTests : DownloadTests +{ + protected override GCSContainer Build() + { + return new GCSBuilder().Build(); + } + + protected override ServiceProvider ConfigureServices() + { + return GCSConfigurator.ConfigureServices(Container.GetConnectionString()); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/GCS/GCSUploadTests.cs b/ManagedCode.Storage.Tests/Storages/GCS/GCSUploadTests.cs new file mode 100644 index 00000000..24c3b457 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/GCS/GCSUploadTests.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using TestcontainersGCS; +using Xunit; + +namespace ManagedCode.Storage.Tests.Storages.GCS; + +[Collection("Google")] +public class GCSUploadTests : UploadTests +{ + protected override GCSContainer Build() + { + return new GCSBuilder().Build(); + } + + protected override ServiceProvider ConfigureServices() + { + return GCSConfigurator.ConfigureServices(Container.GetConnectionString()); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/Storages/UploadTests.cs b/ManagedCode.Storage.Tests/Storages/UploadTests.cs new file mode 100644 index 00000000..cb6db568 --- /dev/null +++ b/ManagedCode.Storage.Tests/Storages/UploadTests.cs @@ -0,0 +1,144 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; +using DotNet.Testcontainers.Containers; +using FluentAssertions; +using ManagedCode.Storage.Core.Models; +using ManagedCode.Storage.Tests.Common; +using Xunit; + +// ReSharper disable MethodHasAsyncOverload + +namespace ManagedCode.Storage.Tests.Storages; + +public abstract class UploadTests : BaseContainer where T : IContainer +{ + [Fact] + public async Task UploadAsync_AsText_WithoutOptions() + { + // Arrange + var uploadContent = FileHelper.GenerateRandomFileContent(); + + // Act + var result = await Storage.UploadAsync(uploadContent); + var downloadedResult = await Storage.DownloadAsync(result.Value!.Name); + + // Assert + result.IsSuccess.Should().BeTrue(); + downloadedResult.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task UploadAsync_AsStream_WithoutOptions() + { + // Arrange + var uploadContent = FileHelper.GenerateRandomFileContent(); + var byteArray = Encoding.ASCII.GetBytes(uploadContent); + var stream = new MemoryStream(byteArray); + + // Act + var result = await Storage.UploadAsync(stream); + var downloadedResult = await Storage.DownloadAsync(result.Value!.Name); + + // Assert + result.IsSuccess.Should().BeTrue(); + downloadedResult.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task StreamUploadAsyncTest() + { + var file = await GetTestFileAsync(); + var uploadResult = await Storage.UploadAsync(file.OpenRead()); + uploadResult.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task ArrayUploadAsyncTest() + { + var file = await GetTestFileAsync(); + var bytes = await File.ReadAllBytesAsync(file.FullName); + var uploadResult = await Storage.UploadAsync(bytes); + uploadResult.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task StringUploadAsyncTest() + { + var file = await GetTestFileAsync(); + var text = await File.ReadAllTextAsync(file.FullName); + var uploadResult = await Storage.UploadAsync(text); + uploadResult.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task FileInfoUploadAsyncTest() + { + var file = await GetTestFileAsync(); + var uploadResult = await Storage.UploadAsync(file); + uploadResult.IsSuccess.Should().BeTrue(); + + var downloadResult = await Storage.DownloadAsync(uploadResult.Value!.Name); + downloadResult.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task UploadAsync_AsStream_WithOptions_ToDirectory_SpecifyingFileName() + { + // Arrange + var directory = "test-directory"; + var uploadContent = FileHelper.GenerateRandomFileContent(); + var fileName = FileHelper.GenerateRandomFileName(); + + var byteArray = Encoding.ASCII.GetBytes(uploadContent); + var stream = new MemoryStream(byteArray); + + // Act + var result = await Storage.UploadAsync(stream, new UploadOptions { FileName = fileName, Directory = directory }); + var downloadedResult = await Storage.DownloadAsync(new DownloadOptions { FileName = fileName, Directory = directory }); + + // Assert + result.IsSuccess.Should().BeTrue(); + downloadedResult.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task UploadAsync_AsArray_WithOptions_ToDirectory_SpecifyingFileName() + { + // Arrange + var directory = "test-directory"; + var uploadContent = FileHelper.GenerateRandomFileContent(); + var fileName = FileHelper.GenerateRandomFileName(); + + var byteArray = Encoding.ASCII.GetBytes(uploadContent); + + // Act + var result = await Storage.UploadAsync(byteArray, new UploadOptions { FileName = fileName, Directory = directory }); + var downloadedResult = await Storage.DownloadAsync(new DownloadOptions { FileName = fileName, Directory = directory }); + + // Assert + result.IsSuccess.Should().BeTrue(); + downloadedResult.IsSuccess.Should().BeTrue(); + + await Storage.DeleteAsync(fileName); + } + + [Fact] + public async Task UploadAsync_AsText_WithOptions_ToDirectory_SpecifyingFileName() + { + // Arrange + var directory = "test-directory"; + var uploadContent = FileHelper.GenerateRandomFileContent(); + var fileName = FileHelper.GenerateRandomFileName(); + + // Act + var result = await Storage.UploadAsync(uploadContent, new UploadOptions { FileName = fileName, Directory = directory }); + var downloadedResult = await Storage.DownloadAsync(new DownloadOptions { FileName = fileName, Directory = directory }); + + // Assert + result.IsSuccess.Should().BeTrue(); + downloadedResult.IsSuccess.Should().BeTrue(); + + await Storage.DeleteAsync(fileName); + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Tests/test.csv b/ManagedCode.Storage.Tests/test.csv deleted file mode 100644 index 8e288270..00000000 --- a/ManagedCode.Storage.Tests/test.csv +++ /dev/null @@ -1,3 +0,0 @@ -TimeStamp,TrackerId,Event,Host,Screen,Language,Url,EventType,EventValue,UserAgent,City,Country -07/13/2022 22:15:11 +00:00,web-site-id-keyload,pageview,api.keyloader.cloud,1792x1120,"en-GB,en;q=0.9,en-US;q=0.8,ru;q=0.7,uk;q=0.6",/login,,,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.51",, -07/13/2022 22:15:11 +00:00,web-site-id-keyload,pageview,api.keyloader.cloud,1792x1120,"en-GB,en;q=0.9,en-US;q=0.8,ru;q=0.7,uk;q=0.6",/login,,,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.51",, diff --git a/ManagedCode.Storage.sln b/ManagedCode.Storage.sln index 1ffa5e77..0e216534 100644 --- a/ManagedCode.Storage.sln +++ b/ManagedCode.Storage.sln @@ -11,20 +11,30 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedCode.Storage.Tests", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedCode.Storage.Aws", "ManagedCode.Storage.Aws\ManagedCode.Storage.Aws.csproj", "{0AFE156D-0DA5-4B23-8262-CA98E4C0FB5F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedCode.Storage.Gcp", "ManagedCode.Storage.Gcp\ManagedCode.Storage.Gcp.csproj", "{C3B4FF9C-1C6A-4EA0-9291-E7E0C0EF2BA3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedCode.Storage.Google", "ManagedCode.Storage.Google\ManagedCode.Storage.Google.csproj", "{C3B4FF9C-1C6A-4EA0-9291-E7E0C0EF2BA3}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagedCode.Storage.FileSystem", "ManagedCode.Storage.FileSystem\ManagedCode.Storage.FileSystem.csproj", "{EDFA1CB7-1721-4447-9C25-AE110821717C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagedCode.Storage.AspNetExtensions", "ManagedCode.Storage.AspNetExtensions\ManagedCode.Storage.AspNetExtensions.csproj", "{852B0DBD-37F0-4DC0-B966-C284AE03C2F5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagedCode.Storage.Server", "ManagedCode.Storage.Server\ManagedCode.Storage.Server.csproj", "{852B0DBD-37F0-4DC0-B966-C284AE03C2F5}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{C812D2D8-1854-4043-94A9-4AA32D606D68}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiSample", "Samples\WebApiSample\WebApiSample.csproj", "{39E5D805-04B4-442B-9B64-116A96041A93}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagedCode.Storage.AzureDataLake", "ManagedCode.Storage.AzureDataLake\ManagedCode.Storage.AzureDataLake.csproj", "{4D4D2AC7-923D-4219-9BC9-341FBA7FE690}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagedCode.Storage.Azure.DataLake", "ManagedCode.Storage.Azure.DataLake\ManagedCode.Storage.Azure.DataLake.csproj", "{4D4D2AC7-923D-4219-9BC9-341FBA7FE690}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagedCode.Storage.TestFakes", "ManagedCode.Storage.TestFakes\ManagedCode.Storage.TestFakes.csproj", "{7190B548-4BE9-4EF6-B55F-8432757AEAD5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestcontainersGCS", "TestcontainersGCS\TestcontainersGCS.csproj", "{40638DBB-CB6F-4B11-B4B5-50446CE426E7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Storages", "Storages", "{92201402-E361-440F-95DB-68663D228C2D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNet", "AspNet", "{94DB7354-F5C7-4347-B9EC-FCCA38B86876}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagedCode.Storage.Client", "ManagedCode.Storage.Client\ManagedCode.Storage.Client.csproj", "{D5A7D3A7-E6E8-4153-911D-D7C0C5C8B19C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagedCode.Storage.Client.SignalR", "ManagedCode.Storage.Client.SignalR\ManagedCode.Storage.Client.SignalR.csproj", "{ED216AAD-CBA2-40F2-AA01-63C60E906632}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -71,6 +81,18 @@ Global {7190B548-4BE9-4EF6-B55F-8432757AEAD5}.Debug|Any CPU.Build.0 = Debug|Any CPU {7190B548-4BE9-4EF6-B55F-8432757AEAD5}.Release|Any CPU.ActiveCfg = Release|Any CPU {7190B548-4BE9-4EF6-B55F-8432757AEAD5}.Release|Any CPU.Build.0 = Release|Any CPU + {40638DBB-CB6F-4B11-B4B5-50446CE426E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40638DBB-CB6F-4B11-B4B5-50446CE426E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40638DBB-CB6F-4B11-B4B5-50446CE426E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40638DBB-CB6F-4B11-B4B5-50446CE426E7}.Release|Any CPU.Build.0 = Release|Any CPU + {D5A7D3A7-E6E8-4153-911D-D7C0C5C8B19C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5A7D3A7-E6E8-4153-911D-D7C0C5C8B19C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5A7D3A7-E6E8-4153-911D-D7C0C5C8B19C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5A7D3A7-E6E8-4153-911D-D7C0C5C8B19C}.Release|Any CPU.Build.0 = Release|Any CPU + {ED216AAD-CBA2-40F2-AA01-63C60E906632}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED216AAD-CBA2-40F2-AA01-63C60E906632}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED216AAD-CBA2-40F2-AA01-63C60E906632}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED216AAD-CBA2-40F2-AA01-63C60E906632}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -80,5 +102,13 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {39E5D805-04B4-442B-9B64-116A96041A93} = {C812D2D8-1854-4043-94A9-4AA32D606D68} + {C3B4FF9C-1C6A-4EA0-9291-E7E0C0EF2BA3} = {92201402-E361-440F-95DB-68663D228C2D} + {4D4D2AC7-923D-4219-9BC9-341FBA7FE690} = {92201402-E361-440F-95DB-68663D228C2D} + {0D6304D1-911D-489E-A716-6CBD5D0FE05D} = {92201402-E361-440F-95DB-68663D228C2D} + {0AFE156D-0DA5-4B23-8262-CA98E4C0FB5F} = {92201402-E361-440F-95DB-68663D228C2D} + {EDFA1CB7-1721-4447-9C25-AE110821717C} = {92201402-E361-440F-95DB-68663D228C2D} + {852B0DBD-37F0-4DC0-B966-C284AE03C2F5} = {94DB7354-F5C7-4347-B9EC-FCCA38B86876} + {D5A7D3A7-E6E8-4153-911D-D7C0C5C8B19C} = {94DB7354-F5C7-4347-B9EC-FCCA38B86876} + {ED216AAD-CBA2-40F2-AA01-63C60E906632} = {94DB7354-F5C7-4347-B9EC-FCCA38B86876} EndGlobalSection EndGlobal diff --git a/Samples/WebApiSample/Controllers/GCPStorageController.cs b/Samples/WebApiSample/Controllers/GCPStorageController.cs index 0db54c77..2ac739d7 100644 --- a/Samples/WebApiSample/Controllers/GCPStorageController.cs +++ b/Samples/WebApiSample/Controllers/GCPStorageController.cs @@ -1,4 +1,4 @@ -using ManagedCode.Storage.Gcp; +using ManagedCode.Storage.Google; using Microsoft.AspNetCore.Mvc; namespace WebApiSample.Controllers; diff --git a/Samples/WebApiSample/Controllers/StorageController.cs b/Samples/WebApiSample/Controllers/StorageController.cs index aff2e36b..e0f7dd0e 100644 --- a/Samples/WebApiSample/Controllers/StorageController.cs +++ b/Samples/WebApiSample/Controllers/StorageController.cs @@ -1,4 +1,4 @@ -using ManagedCode.Storage.AspNetExtensions; +using ManagedCode.Storage.Server; using ManagedCode.Storage.Core; using Microsoft.AspNetCore.Mvc; diff --git a/Samples/WebApiSample/Program.cs b/Samples/WebApiSample/Program.cs index 04db806d..10a97c71 100644 --- a/Samples/WebApiSample/Program.cs +++ b/Samples/WebApiSample/Program.cs @@ -5,8 +5,8 @@ using ManagedCode.Storage.Azure.Options; using ManagedCode.Storage.FileSystem.Extensions; using ManagedCode.Storage.FileSystem.Options; -using ManagedCode.Storage.Gcp.Extensions; -using ManagedCode.Storage.Gcp.Options; +using ManagedCode.Storage.Google.Extensions; +using ManagedCode.Storage.Google.Options; var builder = WebApplication.CreateBuilder(args); diff --git a/Samples/WebApiSample/WebApiSample.csproj b/Samples/WebApiSample/WebApiSample.csproj index 424a24ea..1ee2d993 100644 --- a/Samples/WebApiSample/WebApiSample.csproj +++ b/Samples/WebApiSample/WebApiSample.csproj @@ -13,12 +13,12 @@ - + - + diff --git a/TestcontainersGCS/GCSBuilder.cs b/TestcontainersGCS/GCSBuilder.cs new file mode 100644 index 00000000..eef6bf56 --- /dev/null +++ b/TestcontainersGCS/GCSBuilder.cs @@ -0,0 +1,70 @@ +namespace TestcontainersGCS; + +/// +[PublicAPI] +public sealed class GCSBuilder : ContainerBuilder +{ + public const string GCSImage = "fsouza/fake-gcs-server:1.47.5"; + public const ushort GCSPort = 30000; + + /// + /// Initializes a new instance of the class. + /// + public GCSBuilder() + : this(new GCSConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private GCSBuilder(GCSConfiguration dockerResourceConfiguration) + : base(dockerResourceConfiguration) + { + DockerResourceConfiguration = dockerResourceConfiguration; + } + + /// + protected override GCSConfiguration DockerResourceConfiguration { get; } + + /// + public override GCSContainer Build() + { + Validate(); + return new GCSContainer(DockerResourceConfiguration, TestcontainersSettings.Logger); + } + + /// + protected override GCSBuilder Init() + { + return base.Init() + .WithImage(GCSImage) + .WithPortBinding(GCSPort, GCSPort) + .WithCommand("-scheme", "http") + .WithCommand("-backend", "memory") + .WithCommand("-external-url", $"http://localhost:{GCSPort}") + .WithCommand("-port", $"{GCSPort}") + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => + request.ForPath("/").ForPort(GCSPort).ForStatusCode(HttpStatusCode.NotFound))); + } + + /// + protected override GCSBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new GCSConfiguration(resourceConfiguration)); + } + + /// + protected override GCSBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new GCSConfiguration(resourceConfiguration)); + } + + /// + protected override GCSBuilder Merge(GCSConfiguration oldValue, GCSConfiguration newValue) + { + return new GCSBuilder(new GCSConfiguration(oldValue, newValue)); + } +} \ No newline at end of file diff --git a/TestcontainersGCS/GCSConfiguration.cs b/TestcontainersGCS/GCSConfiguration.cs new file mode 100644 index 00000000..047bdb96 --- /dev/null +++ b/TestcontainersGCS/GCSConfiguration.cs @@ -0,0 +1,53 @@ +namespace TestcontainersGCS; + +/// +[PublicAPI] +public sealed class GCSConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + public GCSConfiguration() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public GCSConfiguration(IResourceConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public GCSConfiguration(IContainerConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public GCSConfiguration(GCSConfiguration resourceConfiguration) + : this(new GCSConfiguration(), resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The old Docker resource configuration. + /// The new Docker resource configuration. + public GCSConfiguration(GCSConfiguration oldValue, GCSConfiguration newValue) + : base(oldValue, newValue) + { + } +} \ No newline at end of file diff --git a/TestcontainersGCS/GCSContainer.cs b/TestcontainersGCS/GCSContainer.cs new file mode 100644 index 00000000..2a7cf485 --- /dev/null +++ b/TestcontainersGCS/GCSContainer.cs @@ -0,0 +1,26 @@ +namespace TestcontainersGCS; + +/// +[PublicAPI] +public sealed class GCSContainer : DockerContainer +{ + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + /// The logger. + public GCSContainer(GCSConfiguration configuration, ILogger logger) + : base(configuration, logger) + { + } + + /// + /// Gets the GCS connection string. + /// + /// The GCS connection string. + public string GetConnectionString() + { + var builder = new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(GCSBuilder.GCSPort), "storage/v1/"); + return builder.ToString(); + } +} \ No newline at end of file diff --git a/TestcontainersGCS/TestcontainersGCS.csproj b/TestcontainersGCS/TestcontainersGCS.csproj new file mode 100644 index 00000000..d3f0266d --- /dev/null +++ b/TestcontainersGCS/TestcontainersGCS.csproj @@ -0,0 +1,15 @@ + + + net7.0 + latest + false + + + + + + + + + + \ No newline at end of file diff --git a/TestcontainersGCS/Usings.cs b/TestcontainersGCS/Usings.cs new file mode 100644 index 00000000..49507dd3 --- /dev/null +++ b/TestcontainersGCS/Usings.cs @@ -0,0 +1,8 @@ +global using System; +global using System.Net; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; +global using Microsoft.Extensions.Logging; \ No newline at end of file