Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blob storing feature #6708

Merged
merged 11 commits into from
Nov 10, 2023
42 changes: 42 additions & 0 deletions Abp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Abp.ZeroCore.NHibernate", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Abp.HtmlSanitizer", "src\Abp.HtmlSanitizer\Abp.HtmlSanitizer.csproj", "{BC2B3FA8-40D1-49AF-AD73-703A163F1BAD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abp.BlobStoring", "src\Abp.BlobStoring\Abp.BlobStoring.csproj", "{0A5E11F6-02FC-4244-90C2-888209506694}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abp.BlobStoring.FileSystem", "src\Abp.BlobStoring.FileSystem\Abp.BlobStoring.FileSystem.csproj", "{31BCFB0E-8930-49DE-9587-DA83B3968CFB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abp.BlobStoring.Azure", "src\Abp.BlobStoring.Azure\Abp.BlobStoring.Azure.csproj", "{E262671E-572A-41A6-8FF5-E77FEF6F22DE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abp.BlobStoring.Tests", "test\Abp.BlobStoring.Tests\Abp.BlobStoring.Tests.csproj", "{83033E58-88EB-4746-A15D-02E3DC1BB03A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abp.BlobStoring.FileSystem.Tests", "test\Abp.BlobStoring.FileSystem.Tests\Abp.BlobStoring.FileSystem.Tests.csproj", "{3B7382DA-99D4-48AA-B8B9-16D905D60BFF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abp.BlobStoring.Azure.Tests", "test\Abp.BlobStoring.Azure.Tests\Abp.BlobStoring.Azure.Tests.csproj", "{168BE615-DA0F-42FA-98F4-6694034DBF8A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -499,6 +511,30 @@ Global
{BC2B3FA8-40D1-49AF-AD73-703A163F1BAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC2B3FA8-40D1-49AF-AD73-703A163F1BAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC2B3FA8-40D1-49AF-AD73-703A163F1BAD}.Release|Any CPU.Build.0 = Release|Any CPU
{0A5E11F6-02FC-4244-90C2-888209506694}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0A5E11F6-02FC-4244-90C2-888209506694}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A5E11F6-02FC-4244-90C2-888209506694}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A5E11F6-02FC-4244-90C2-888209506694}.Release|Any CPU.Build.0 = Release|Any CPU
{31BCFB0E-8930-49DE-9587-DA83B3968CFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{31BCFB0E-8930-49DE-9587-DA83B3968CFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31BCFB0E-8930-49DE-9587-DA83B3968CFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31BCFB0E-8930-49DE-9587-DA83B3968CFB}.Release|Any CPU.Build.0 = Release|Any CPU
{E262671E-572A-41A6-8FF5-E77FEF6F22DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E262671E-572A-41A6-8FF5-E77FEF6F22DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E262671E-572A-41A6-8FF5-E77FEF6F22DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E262671E-572A-41A6-8FF5-E77FEF6F22DE}.Release|Any CPU.Build.0 = Release|Any CPU
{83033E58-88EB-4746-A15D-02E3DC1BB03A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{83033E58-88EB-4746-A15D-02E3DC1BB03A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{83033E58-88EB-4746-A15D-02E3DC1BB03A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{83033E58-88EB-4746-A15D-02E3DC1BB03A}.Release|Any CPU.Build.0 = Release|Any CPU
{3B7382DA-99D4-48AA-B8B9-16D905D60BFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B7382DA-99D4-48AA-B8B9-16D905D60BFF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B7382DA-99D4-48AA-B8B9-16D905D60BFF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B7382DA-99D4-48AA-B8B9-16D905D60BFF}.Release|Any CPU.Build.0 = Release|Any CPU
{168BE615-DA0F-42FA-98F4-6694034DBF8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{168BE615-DA0F-42FA-98F4-6694034DBF8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{168BE615-DA0F-42FA-98F4-6694034DBF8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{168BE615-DA0F-42FA-98F4-6694034DBF8A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -584,6 +620,12 @@ Global
{DE3D4F5B-70A2-488B-A478-2C73FFE36D46} = {DFF0464B-5402-4DD6-86F5-2AEC1163B232}
{01F71E3D-5010-43AA-870A-ADED860FF73B} = {DFF0464B-5402-4DD6-86F5-2AEC1163B232}
{BC2B3FA8-40D1-49AF-AD73-703A163F1BAD} = {DFF0464B-5402-4DD6-86F5-2AEC1163B232}
{0A5E11F6-02FC-4244-90C2-888209506694} = {DFF0464B-5402-4DD6-86F5-2AEC1163B232}
{31BCFB0E-8930-49DE-9587-DA83B3968CFB} = {DFF0464B-5402-4DD6-86F5-2AEC1163B232}
{E262671E-572A-41A6-8FF5-E77FEF6F22DE} = {DFF0464B-5402-4DD6-86F5-2AEC1163B232}
{83033E58-88EB-4746-A15D-02E3DC1BB03A} = {1E6B9E8D-D5C1-4AD7-89F9-7179FEFD7C01}
{3B7382DA-99D4-48AA-B8B9-16D905D60BFF} = {1E6B9E8D-D5C1-4AD7-89F9-7179FEFD7C01}
{168BE615-DA0F-42FA-98F4-6694034DBF8A} = {1E6B9E8D-D5C1-4AD7-89F9-7179FEFD7C01}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5A1CB38E-F77D-4A40-B3A9-9A70C3F3BC6D}
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' ">
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>

Expand Down
3 changes: 3 additions & 0 deletions nupkg/pack.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ $projects = (
"Abp.AspNetCore.TestBase",
"Abp.AspNetCore.PerRequestRedisCache",
"Abp.AutoMapper",
"Abp.BlobStoring",
"Abp.BlobStoring.Azure",
"Abp.BlobStoring.FileSystem",
"Abp.Castle.Log4Net",
"Abp.Dapper",
"Abp.EntityFramework",
Expand Down
39 changes: 39 additions & 0 deletions src/Abp.BlobStoring.Azure/Abp.BlobStoring.Azure.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\common.props" />
<Import Project="..\..\configureawait.props" />

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AssemblyName>Abp.BlobStoring.Azure</AssemblyName>
<PackageId>Abp.BlobStoring.Azure</PackageId>
<PackageTags>asp.net;asp.net mvc;boilerplate;application framework;web framework;framework;domain driven design;blob storing; azure</PackageTags>
<GenerateAssemblyTitleAttribute>false</GenerateAssemblyTitleAttribute>
<GenerateAssemblyDescriptionAttribute>false</GenerateAssemblyDescriptionAttribute>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<GenerateAssemblyCopyrightAttribute>false</GenerateAssemblyCopyrightAttribute>
<PackageRequireLicenseAcceptance>False</PackageRequireLicenseAcceptance>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<RootNamespace>Abp</RootNamespace>
<Description>Abp.BlobStoring.Azure</Description>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Abp.BlobStoring\Abp.BlobStoring.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" Version="12.19.0" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="Fody" Version="6.8.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Update="Microsoft.SourceLink.GitHub" Version="1.1.1" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Reflection;
using Abp.Modules;

namespace Abp.BlobStoring.Azure
{
[DependsOn(typeof(AbpBlobStoringModule))]
public class AbpBlobStoringAzureModule : AbpModule
{
public override void Initialize()
{
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace Abp.BlobStoring.Azure
{
public static class AzureBlobContainerConfigurationExtensions
{
public static AzureBlobProviderConfiguration GetAzureConfiguration(
this BlobContainerConfiguration containerConfiguration)
{
return new AzureBlobProviderConfiguration(containerConfiguration);
}

public static BlobContainerConfiguration UseAzure(
this BlobContainerConfiguration containerConfiguration,
Action<AzureBlobProviderConfiguration> azureConfigureAction)
{
containerConfiguration.ProviderType = typeof(AzureBlobProvider);
containerConfiguration.NamingNormalizers.TryAdd<AzureBlobNamingNormalizer>();

azureConfigureAction(new AzureBlobProviderConfiguration(containerConfiguration));

return containerConfiguration;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Globalization;
using System.Text.RegularExpressions;
using Abp.Localization;
using Abp.Dependency;

namespace Abp.BlobStoring.Azure
{
public class AzureBlobNamingNormalizer : IBlobNamingNormalizer, ITransientDependency
{
/// <summary>
///https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names
/// </summary>
public virtual string NormalizeContainerName(string containerName)
{
using (CultureInfoHelper.Use(CultureInfo.InvariantCulture))
{
// All letters in a container name must be lowercase.
containerName = containerName.ToLower();

// Container names must be from 3 through 63 characters long.
if (containerName.Length > 63)
{
containerName = containerName.Substring(0, 63);
}

// Container names can contain only letters, numbers, and the dash (-) character.
containerName = Regex.Replace(containerName, "[^a-z0-9-]", string.Empty);

// Every dash (-) character must be immediately preceded and followed by a letter or number;
// consecutive dashes are not permitted in container names.
// Container names must start or end with a letter or number
containerName = Regex.Replace(containerName, "-{2,}", "-");
containerName = Regex.Replace(containerName, "^-", string.Empty);
containerName = Regex.Replace(containerName, "-$", string.Empty);

// Container names must be from 3 through 63 characters long.
if (containerName.Length < 3)
{
var length = containerName.Length;
for (var i = 0; i < 3 - length; i++)
{
containerName += "0";
}
}

return containerName;
}
}

/// <summary>
///https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#blob-names
/// </summary>
public virtual string NormalizeBlobName(string blobName)
{
return blobName;
}
}
}
112 changes: 112 additions & 0 deletions src/Abp.BlobStoring.Azure/BlobStoring/Azure/AzureBlobProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using Abp.Dependency;
using Abp.Extensions;
using Azure.Storage.Blobs;
using System.IO;
using System.Threading.Tasks;

namespace Abp.BlobStoring.Azure
{
public class AzureBlobProvider : BlobProviderBase, ITransientDependency
{
protected IAzureBlobNameCalculator AzureBlobNameCalculator { get; }
protected IBlobNormalizeNamingService BlobNormalizeNamingService { get; }

public AzureBlobProvider(
IAzureBlobNameCalculator azureBlobNameCalculator,
IBlobNormalizeNamingService blobNormalizeNamingService)
{
AzureBlobNameCalculator = azureBlobNameCalculator;
BlobNormalizeNamingService = blobNormalizeNamingService;
}

public override async Task SaveAsync(BlobProviderSaveArgs args)
{
var blobName = AzureBlobNameCalculator.Calculate(args);
var configuration = args.Configuration.GetAzureConfiguration();

if (!args.OverrideExisting && await BlobExistsAsync(args, blobName))
{
throw new BlobAlreadyExistsException($"Saving BLOB '{args.BlobName}' does already exists in the container '{GetContainerName(args)}'! Set {nameof(args.OverrideExisting)} if it should be overwritten.");
}

if (configuration.CreateContainerIfNotExists)
{
await CreateContainerIfNotExists(args);
}

await GetBlobClient(args, blobName).UploadAsync(args.BlobStream, true);
}

public override async Task<bool> DeleteAsync(BlobProviderDeleteArgs args)
{
var blobName = AzureBlobNameCalculator.Calculate(args);

if (await BlobExistsAsync(args, blobName))
{
return await GetBlobClient(args, blobName).DeleteIfExistsAsync();
}

return false;
}

public override async Task<bool> ExistsAsync(BlobProviderExistsArgs args)
{
var blobName = AzureBlobNameCalculator.Calculate(args);

return await BlobExistsAsync(args, blobName);
}

public override async Task<Stream> GetOrNullAsync(BlobProviderGetArgs args)
{
var blobName = AzureBlobNameCalculator.Calculate(args);

if (!await BlobExistsAsync(args, blobName))
{
return null;
}

var blobClient = GetBlobClient(args, blobName);
var download = await blobClient.DownloadAsync();
return await TryCopyToMemoryStreamAsync(download.Value.Content, args.CancellationToken);
}

protected virtual BlobClient GetBlobClient(BlobProviderArgs args, string blobName)
{
var blobContainerClient = GetBlobContainerClient(args);
return blobContainerClient.GetBlobClient(blobName);
}

protected virtual BlobContainerClient GetBlobContainerClient(BlobProviderArgs args)
{
var configuration = args.Configuration.GetAzureConfiguration();
var blobServiceClient = new BlobServiceClient(configuration.ConnectionString);
return blobServiceClient.GetBlobContainerClient(GetContainerName(args));
}

protected virtual async Task CreateContainerIfNotExists(BlobProviderArgs args)
{
var blobContainerClient = GetBlobContainerClient(args);
await blobContainerClient.CreateIfNotExistsAsync();
}

protected virtual async Task<bool> BlobExistsAsync(BlobProviderArgs args, string blobName)
{
// Make sure Blob Container exists.
return await ContainerExistsAsync(GetBlobContainerClient(args)) &&
(await GetBlobClient(args, blobName).ExistsAsync()).Value;
}

protected virtual string GetContainerName(BlobProviderArgs args)
{
var configuration = args.Configuration.GetAzureConfiguration();
return configuration.ContainerName.IsNullOrWhiteSpace()
? args.ContainerName
: BlobNormalizeNamingService.NormalizeContainerName(args.Configuration, configuration.ContainerName);
}

protected virtual async Task<bool> ContainerExistsAsync(BlobContainerClient blobContainerClient)
{
return (await blobContainerClient.ExistsAsync()).Value;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Abp.BlobStoring.Azure
{
public class AzureBlobProviderConfiguration
{
public string ConnectionString
{
get => _containerConfiguration.GetConfiguration<string>(AzureBlobProviderConfigurationNames.ConnectionString);
set => _containerConfiguration.SetConfiguration(AzureBlobProviderConfigurationNames.ConnectionString, Check.NotNullOrWhiteSpace(value, nameof(value)));
}

/// <summary>
/// This name may only contain lowercase letters, numbers, and hyphens, and must begin with a letter or a number.
/// Each hyphen must be preceded and followed by a non-hyphen character.
/// The name must also be between 3 and 63 characters long.
/// If this parameter is not specified, the ContainerName of the <see cref="BlobProviderArgs"/> will be used.
/// </summary>
public string ContainerName
{
get => _containerConfiguration.GetConfigurationOrDefault<string>(AzureBlobProviderConfigurationNames.ContainerName);
set => _containerConfiguration.SetConfiguration(AzureBlobProviderConfigurationNames.ContainerName, value);
}

/// <summary>
/// Default value: false.
/// </summary>
public bool CreateContainerIfNotExists
{
get => _containerConfiguration.GetConfigurationOrDefault(AzureBlobProviderConfigurationNames.CreateContainerIfNotExists, false);
set => _containerConfiguration.SetConfiguration(AzureBlobProviderConfigurationNames.CreateContainerIfNotExists, value);
}

private readonly BlobContainerConfiguration _containerConfiguration;

public AzureBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration)
{
_containerConfiguration = containerConfiguration;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Abp.BlobStoring.Azure
{
public static class AzureBlobProviderConfigurationNames
{
public const string ConnectionString = "Azure.ConnectionString";
public const string ContainerName = "Azure.ContainerName";
public const string CreateContainerIfNotExists = "Azure.CreateContainerIfNotExists";
}
}