diff --git a/src/FileManagement/uBeac.Core.FileManagement.Abstractions/IFileRepository.cs b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/IFileRepository.cs new file mode 100644 index 00000000..35b3861e --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/IFileRepository.cs @@ -0,0 +1,15 @@ +using uBeac.Repositories; + +namespace uBeac.FileManagement; + +public interface IFileRepository : IEntityRepository + where TKey : IEquatable + where TEntity : IFileEntity +{ + Task> Search(SearchFileRequest request, CancellationToken cancellationToken = default); +} + +public interface IFileRepository : IFileRepository, IEntityRepository + where TEntity : IFileEntity +{ +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement.Abstractions/IFileService.cs b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/IFileService.cs new file mode 100644 index 00000000..7ee32fd9 --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/IFileService.cs @@ -0,0 +1,23 @@ +using uBeac.Services; + +namespace uBeac.FileManagement; + +public interface IFileService : IService +{ + Task> Search(SearchFileRequest request, CancellationToken cancellationToken = default); + Task Get(GetFileRequest request, CancellationToken cancellationToken = default); + Task Create(FileModel model, CancellationToken cancellationToken = default); +} + +public interface IFileService : IFileService + where TKey : IEquatable + where TEntity : IFileEntity +{ + Task> Search(SearchFileRequest request, CancellationToken cancellationToken = default); + Task Create(FileModel model, TEntity entity, CancellationToken cancellationToken = default); +} + +public interface IFileService : IFileService + where TEntity : IFileEntity +{ +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement.Abstractions/IFileStorageProvider.cs b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/IFileStorageProvider.cs new file mode 100644 index 00000000..72899f83 --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/IFileStorageProvider.cs @@ -0,0 +1,9 @@ +namespace uBeac.FileManagement; + +public interface IFileStorageProvider +{ + string Name { get; } + + Task Create(Stream stream, string fileName, CancellationToken cancellationToken = default); + Task Get(string fileName, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement.Abstractions/IFileValidator.cs b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/IFileValidator.cs new file mode 100644 index 00000000..4092891a --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/IFileValidator.cs @@ -0,0 +1,30 @@ +namespace uBeac.FileManagement; + +public interface IFileValidator +{ + IFileValidationResult Validate(FileModel model); +} + +public interface IFileValidationResult +{ + bool Validated { get; set; } + Exception Exception { get; set; } +} + +public class FileValidationResult : IFileValidationResult +{ + public FileValidationResult() + { + Validated = true; + Exception = null; + } + + public FileValidationResult(Exception exception) + { + Validated = false; + Exception = exception; + } + + public bool Validated { get; set; } + public Exception Exception { get; set; } +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement.Abstractions/Models/FileEntity.cs b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/Models/FileEntity.cs new file mode 100644 index 00000000..8eb60f1e --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/Models/FileEntity.cs @@ -0,0 +1,27 @@ +namespace uBeac; + +public interface IFileEntity : IAuditEntity + where TKey : IEquatable +{ + string Name { get; set; } + string Extension { get; set; } + string Provider { get; set; } + string Category { get; set; } +} + +public interface IFileEntity : IFileEntity, IAuditEntity +{ +} + +public class FileEntity : AuditEntity, IFileEntity + where TKey : IEquatable +{ + public string Name { get; set; } + public string Extension { get; set; } + public string Provider { get; set; } + public string Category { get; set; } +} + +public class FileEntity : FileEntity, IFileEntity +{ +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement.Abstractions/Models/FileModel.cs b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/Models/FileModel.cs new file mode 100644 index 00000000..dd5ccd4e --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/Models/FileModel.cs @@ -0,0 +1,12 @@ +namespace uBeac.FileManagement; + +public class FileModel : IDisposable, IAsyncDisposable +{ + public Stream Stream { get; set; } + public string Extension { get; set; } + public string Category { get; set; } + + public void Dispose() => Stream.Dispose(); + + public async ValueTask DisposeAsync() => await Stream.DisposeAsync(); +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement.Abstractions/Models/GetFileRequest.cs b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/Models/GetFileRequest.cs new file mode 100644 index 00000000..b34c9fca --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/Models/GetFileRequest.cs @@ -0,0 +1,7 @@ +namespace uBeac.FileManagement; + +public class GetFileRequest +{ + public string Name { get; set; } + public string Category { get; set; } +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement.Abstractions/Models/SearchFileRequest.cs b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/Models/SearchFileRequest.cs new file mode 100644 index 00000000..fd687a2d --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/Models/SearchFileRequest.cs @@ -0,0 +1,15 @@ +namespace uBeac.FileManagement; + + +public class SearchFileRequest where TKey : IEquatable +{ + public string Category { get; set; } + public IEnumerable Ids { get; set; } + public IEnumerable Names { get; set; } + public IEnumerable Extensions { get; set; } + public IEnumerable Providers { get; set; } +} + +public class SearchFileRequest : SearchFileRequest +{ +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement.Abstractions/uBeac.Core.FileManagement.Abstractions.csproj b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/uBeac.Core.FileManagement.Abstractions.csproj new file mode 100644 index 00000000..878147e0 --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement.Abstractions/uBeac.Core.FileManagement.Abstractions.csproj @@ -0,0 +1,20 @@ + + + + uBeac.FileManagement.Abstractions + uBeac + net6.0 + enable + disable + Contains abstractions of file management. + ubeac-logo.jpg + https://github.com/ubeac/ubeac-api + + + + + + + + + diff --git a/src/FileManagement/uBeac.Core.FileManagement.Infrastructure/FileService.cs b/src/FileManagement/uBeac.Core.FileManagement.Infrastructure/FileService.cs new file mode 100644 index 00000000..813bdd94 --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement.Infrastructure/FileService.cs @@ -0,0 +1,75 @@ +namespace uBeac.FileManagement; + +public class FileService : IFileService + where TKey : IEquatable + where TEntity : IFileEntity, new() +{ + protected readonly List Validators; + protected readonly IFileRepository Repository; + protected readonly IFileStorageProvider Provider; + + public FileService(IEnumerable validators, IFileRepository repository, IFileStorageProvider provider) + { + Validators = validators.ToList(); + Repository = repository; + Provider = provider; + } + + public async Task> Search(SearchFileRequest request, CancellationToken cancellationToken = default) + => await Repository.Search(request, cancellationToken); + + public async Task> Search(SearchFileRequest request, CancellationToken cancellationToken = default) + => (IEnumerable) await Search(request as SearchFileRequest, cancellationToken); + + public async Task Get(GetFileRequest request, CancellationToken cancellationToken = default) + { + var entity = (await Search(new SearchFileRequest + { + Category = request.Category, + Names = new[] { request.Name } + }, cancellationToken)).First(); + + return new FileModel + { + Stream = await Provider.Get(entity.Name, cancellationToken), + Category = request.Category, + Extension = entity.Extension + }; + } + + public async Task Create(FileModel model, TEntity entity, CancellationToken cancellationToken = default) + { + ThrowExceptionIfNotValid(model); + + var fileName = Path.GetRandomFileName(); + + entity.Name = fileName; + entity.Extension = model.Extension; + entity.Provider = Provider.Name; + entity.Category = model.Category; + + await Provider.Create(model.Stream, fileName, cancellationToken); + await Repository.Create(entity, cancellationToken); + + return entity; + } + + public async Task Create(FileModel model, CancellationToken cancellationToken = default) + { + return (IFileEntity) await Create(model, new TEntity(), cancellationToken); + } + + protected void ThrowExceptionIfNotValid(FileModel model) => Validators.ForEach(validator => + { + var result = validator.Validate(model); + if (!result.Validated) throw result.Exception; + }); +} + +public class FileService : FileService, IFileService + where TEntity : IFileEntity, new() +{ + public FileService(IEnumerable validators, IFileRepository repository, IFileStorageProvider provider) : base(validators, repository, provider) + { + } +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement.Infrastructure/uBeac.Core.FileManagement.Services.csproj b/src/FileManagement/uBeac.Core.FileManagement.Infrastructure/uBeac.Core.FileManagement.Services.csproj new file mode 100644 index 00000000..66d7b2b8 --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement.Infrastructure/uBeac.Core.FileManagement.Services.csproj @@ -0,0 +1,18 @@ + + + + uBeac.FileManagement.Services + uBeac + net6.0 + enable + disable + Implements file management service. + ubeac-logo.jpg + https://github.com/ubeac/ubeac-api + + + + + + + diff --git a/src/FileManagement/uBeac.Core.FileManagement/Builders/FileManagementBuilder.cs b/src/FileManagement/uBeac.Core.FileManagement/Builders/FileManagementBuilder.cs new file mode 100644 index 00000000..75cac2e2 --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement/Builders/FileManagementBuilder.cs @@ -0,0 +1,27 @@ +using uBeac; +using uBeac.FileManagement; + +namespace Microsoft.Extensions.DependencyInjection; + +public class FileManagementBuilder : IFileManagementBuilder where TKey : IEquatable where TEntity : IFileEntity, new() +{ + protected readonly IServiceCollection Services; + + public FileManagementBuilder(IServiceCollection services) + { + Services = services; + } + + public IFileServiceBuilder AddCategory(string categoryName) + { + var builder = new FileServiceBuilder(); + + Services.AddScoped(serviceProvider => + { + var service = builder.Build(serviceProvider); + return new FileCategory { CategoryName = categoryName, Service = service }; + }); + + return builder; + } +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement/Builders/FileServiceBuilder.cs b/src/FileManagement/uBeac.Core.FileManagement/Builders/FileServiceBuilder.cs new file mode 100644 index 00000000..318c3dc6 --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement/Builders/FileServiceBuilder.cs @@ -0,0 +1,64 @@ +using uBeac; +using uBeac.FileManagement; + +namespace Microsoft.Extensions.DependencyInjection; + +public class FileServiceBuilder : IFileServiceBuilder + where TKey : IEquatable + where TEntity : IFileEntity, new() +{ + public List> Validators { get; } = new(); + public Func> Repository { get; set; } + public Func Provider { get; set; } + + public IFileServiceBuilder AddValidator(IFileValidator validator) + { + Validators.Add(_ => validator); + return this; + } + + public IFileServiceBuilder StoreInfoIn() where TRepository : IFileRepository + { + Repository = serviceProvider => serviceProvider.GetRequiredService(); + return this; + } + + public IFileServiceBuilder StoreInfoIn(TRepository repository) where TRepository : IFileRepository + { + Repository = _ => repository; + return this; + } + + public IFileServiceBuilder StoreInfoIn(Func> builder) + { + Repository = builder; + return this; + } + + public IFileServiceBuilder StoreFilesIn() where TStorageProvider : IFileStorageProvider + { + Provider = serviceProvider => serviceProvider.GetRequiredService(); + return this; + } + + public IFileServiceBuilder StoreFilesIn(TStorageProvider provider) where TStorageProvider : IFileStorageProvider + { + Provider = _ => provider; + return this; + } + + public IFileServiceBuilder StoreFilesIn(Func builder) + { + Provider = builder; + return this; + } + + internal IFileService Build(IServiceProvider serviceProvider) + { + var validators = Validators.Select(validator => validator(serviceProvider)); + var repository = Repository(serviceProvider); + var provider = Provider(serviceProvider); + + return new FileService(validators, repository, provider); + } +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement/Builders/Interfaces/IFileManagementBuilder.cs b/src/FileManagement/uBeac.Core.FileManagement/Builders/Interfaces/IFileManagementBuilder.cs new file mode 100644 index 00000000..e5b5a3b2 --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement/Builders/Interfaces/IFileManagementBuilder.cs @@ -0,0 +1,6 @@ +namespace uBeac.FileManagement; + +public interface IFileManagementBuilder where TKey : IEquatable where TEntity : IFileEntity +{ + IFileServiceBuilder AddCategory(string categoryName); +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement/Builders/Interfaces/IFileServiceBuilder.cs b/src/FileManagement/uBeac.Core.FileManagement/Builders/Interfaces/IFileServiceBuilder.cs new file mode 100644 index 00000000..58645d51 --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement/Builders/Interfaces/IFileServiceBuilder.cs @@ -0,0 +1,16 @@ +namespace uBeac.FileManagement; + +public interface IFileServiceBuilder + where TKey : IEquatable + where TEntity : IFileEntity +{ + IFileServiceBuilder AddValidator(IFileValidator validator); + + IFileServiceBuilder StoreInfoIn() where TRepository : IFileRepository; + IFileServiceBuilder StoreInfoIn(TRepository repository) where TRepository : IFileRepository; + IFileServiceBuilder StoreInfoIn(Func> builder); + + IFileServiceBuilder StoreFilesIn() where TStorageProvider : IFileStorageProvider; + IFileServiceBuilder StoreFilesIn(TStorageProvider provider) where TStorageProvider : IFileStorageProvider; + IFileServiceBuilder StoreFilesIn(Func builder); +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement/Extensions/ServiceCollectionExtensions.cs b/src/FileManagement/uBeac.Core.FileManagement/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..57fc24af --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using uBeac; +using uBeac.FileManagement; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddFileManagement(this IServiceCollection services, Action> options) + where TKey : IEquatable + where TEntity : IFileEntity, new() + { + var builder = new FileManagementBuilder(services); + options.Invoke(builder); + services.TryAddScoped(); + return services; + } + + public static IServiceCollection AddFileManagement(this IServiceCollection services, Action> options) + where TEntity : IFileEntity, new() + => AddFileManagement(services, options); + + public static IServiceCollection AddFileManagement(this IServiceCollection services, Action> options) + => AddFileManagement(services, options); +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement/Models/FileCategory.cs b/src/FileManagement/uBeac.Core.FileManagement/Models/FileCategory.cs new file mode 100644 index 00000000..221335f0 --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement/Models/FileCategory.cs @@ -0,0 +1,7 @@ +namespace uBeac.FileManagement; + +public class FileCategory +{ + public string CategoryName { get; set; } + public IFileService Service { get; set; } +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement/Services/FileManager.cs b/src/FileManagement/uBeac.Core.FileManagement/Services/FileManager.cs new file mode 100644 index 00000000..20f3a96c --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement/Services/FileManager.cs @@ -0,0 +1,55 @@ +namespace uBeac.FileManagement; + +public class FileManager : IFileManager +{ + protected readonly List Categories; + + public FileManager(IEnumerable categories) + { + Categories = categories.ToList(); + } + + public async Task> Search(SearchFileRequest request, CancellationToken cancellationToken = default) + { + var service = GetService(request.Category); + return await service.Search(request, cancellationToken); + } + + public async Task> Search(SearchFileRequest request, CancellationToken cancellationToken = default) where TKey : IEquatable where TEntity : IFileEntity + { + var service = GetService(request.Category); + if (service is not IFileService entityService) throw new Exception("No file service registered for your entity."); + return await entityService.Search(request, cancellationToken); + } + + public async Task> Search(SearchFileRequest request, CancellationToken cancellationToken = default) where TEntity : IFileEntity + { + return await Search(request, cancellationToken); + } + + public async Task Create(FileModel model, CancellationToken cancellationToken = default) + { + var service = GetService(model.Category); + return await service.Create(model, cancellationToken); + } + + public async Task Create(FileModel model, TEntity entity, CancellationToken cancellationToken = default) where TKey : IEquatable where TEntity : IFileEntity + { + var service = GetService(model.Category); + if (service is not IFileService entityService) throw new Exception("No file service registered for your entity."); + return await entityService.Create(model, entity, cancellationToken); + } + + public async Task Create(FileModel model, TEntity entity, CancellationToken cancellationToken = default) where TEntity : IFileEntity + { + return await Create(model, entity, cancellationToken); + } + + public async Task Get(GetFileRequest request, CancellationToken cancellationToken = default) + { + var service = GetService(request.Category); + return await service.Get(request, cancellationToken); + } + + protected IFileService GetService(string category) => Categories.Single(c => c.CategoryName == category).Service; +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement/Services/Interfaces/IFileManager.cs b/src/FileManagement/uBeac.Core.FileManagement/Services/Interfaces/IFileManager.cs new file mode 100644 index 00000000..d02305d9 --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement/Services/Interfaces/IFileManager.cs @@ -0,0 +1,14 @@ +namespace uBeac.FileManagement; + +public interface IFileManager +{ + Task> Search(SearchFileRequest request, CancellationToken cancellationToken = default); + Task> Search(SearchFileRequest request, CancellationToken cancellationToken = default) where TKey : IEquatable where TEntity : IFileEntity; + Task> Search(SearchFileRequest request, CancellationToken cancellationToken = default) where TEntity : IFileEntity; + + Task Create(FileModel model, CancellationToken cancellationToken = default); + Task Create(FileModel model, TEntity entity, CancellationToken cancellationToken = default) where TKey : IEquatable where TEntity : IFileEntity; + Task Create(FileModel model, TEntity entity, CancellationToken cancellationToken = default) where TEntity : IFileEntity; + + Task Get(GetFileRequest request, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement/Validators/FileExtensionValidator.cs b/src/FileManagement/uBeac.Core.FileManagement/Validators/FileExtensionValidator.cs new file mode 100644 index 00000000..cbec3df7 --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement/Validators/FileExtensionValidator.cs @@ -0,0 +1,16 @@ +namespace uBeac.FileManagement; + +public class FileExtensionValidator : IFileValidator +{ + protected readonly string[] ValidExtensions; + + public FileExtensionValidator(IEnumerable validExtensions) + { + ValidExtensions = validExtensions.Select(e => e.ToUpper()).ToArray(); + } + + public IFileValidationResult Validate(FileModel model) + { + return ValidExtensions.Contains(model.Extension.ToUpper()) ? new FileValidationResult() : new FileValidationResult(new Exception("File extension is not valid!")); + } +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement/Validators/FileServiceBuilderExtensions.cs b/src/FileManagement/uBeac.Core.FileManagement/Validators/FileServiceBuilderExtensions.cs new file mode 100644 index 00000000..c00cd2a3 --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement/Validators/FileServiceBuilderExtensions.cs @@ -0,0 +1,15 @@ +using uBeac; +using uBeac.FileManagement; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class FileServiceBuilderExtensions +{ + public static IFileServiceBuilder SetValidExtensions(this IFileServiceBuilder builder, string[] validExtensions) + where TKey : IEquatable + where TEntity : IFileEntity + { + builder.AddValidator(new FileExtensionValidator(validExtensions)); + return builder; + } +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.FileManagement/uBeac.Core.FileManagement.csproj b/src/FileManagement/uBeac.Core.FileManagement/uBeac.Core.FileManagement.csproj new file mode 100644 index 00000000..33bdcc4a --- /dev/null +++ b/src/FileManagement/uBeac.Core.FileManagement/uBeac.Core.FileManagement.csproj @@ -0,0 +1,22 @@ + + + + uBeac.FileManagement + uBeac + net6.0 + enable + disable + Easily implement file management in your .NET projects! + ubeac-logo.jpg + https://github.com/ubeac/ubeac-api + + + + + + + + + + + diff --git a/src/FileManagement/uBeac.Core.Providers.FileManagement.Abstractions/uBeac.Core.Providers.FileManagement.Abstractions.csproj b/src/FileManagement/uBeac.Core.Providers.FileManagement.Abstractions/uBeac.Core.Providers.FileManagement.Abstractions.csproj index 132c02c5..141e38ff 100644 --- a/src/FileManagement/uBeac.Core.Providers.FileManagement.Abstractions/uBeac.Core.Providers.FileManagement.Abstractions.csproj +++ b/src/FileManagement/uBeac.Core.Providers.FileManagement.Abstractions/uBeac.Core.Providers.FileManagement.Abstractions.csproj @@ -3,7 +3,7 @@ net6.0 enable - enable + disable diff --git a/src/FileManagement/uBeac.Core.Providers.FileManagement.LocalDisk/LocalDiskFileProvider.cs b/src/FileManagement/uBeac.Core.Providers.FileManagement.LocalDisk/LocalDiskFileProvider.cs new file mode 100644 index 00000000..769f7b37 --- /dev/null +++ b/src/FileManagement/uBeac.Core.Providers.FileManagement.LocalDisk/LocalDiskFileProvider.cs @@ -0,0 +1,34 @@ +namespace uBeac.FileManagement.LocalStorage; + +public class LocalDiskFileProvider : IFileStorageProvider +{ + protected readonly FileManagementLocalDiskOptions Options; + protected readonly string DirPath; + + public LocalDiskFileProvider(FileManagementLocalDiskOptions options) + { + Options = options; + + DirPath = Path.Combine(Directory.GetCurrentDirectory(), Options.DirectoryPath); + if (!Directory.Exists(DirPath)) Directory.CreateDirectory(DirPath); + } + + public string Name => nameof(LocalDiskFileProvider); + + public async Task Create(Stream stream, string fileName, CancellationToken cancellationToken = default) + { + var path = GetFilePath(fileName); + + await using var createStream = new FileStream(path, FileMode.Create, FileAccess.Write); + await stream.CopyToAsync(createStream, cancellationToken); + + } + + public async Task Get(string fileName, CancellationToken cancellationToken = default) + { + var path = GetFilePath(fileName); + return new FileStream(path, FileMode.Open, FileAccess.Read); ; + } + + protected string GetFilePath(string fileName) => Path.Combine(DirPath, fileName); +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.Providers.FileManagement.LocalDisk/LocalDiskOptions.cs b/src/FileManagement/uBeac.Core.Providers.FileManagement.LocalDisk/LocalDiskOptions.cs new file mode 100644 index 00000000..76d32b4b --- /dev/null +++ b/src/FileManagement/uBeac.Core.Providers.FileManagement.LocalDisk/LocalDiskOptions.cs @@ -0,0 +1,6 @@ +namespace uBeac.FileManagement.LocalStorage; + +public class FileManagementLocalDiskOptions +{ + public string DirectoryPath { get; set; } +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.Providers.FileManagement.LocalDisk/uBeac.Core.FileManagement.Providers.LocalDisk.csproj b/src/FileManagement/uBeac.Core.Providers.FileManagement.LocalDisk/uBeac.Core.FileManagement.Providers.LocalDisk.csproj new file mode 100644 index 00000000..c4cd1891 --- /dev/null +++ b/src/FileManagement/uBeac.Core.Providers.FileManagement.LocalDisk/uBeac.Core.FileManagement.Providers.LocalDisk.csproj @@ -0,0 +1,22 @@ + + + + uBeac.FileManagement.StorageProviders.LocalDisk + uBeac + net6.0 + enable + disable + Used to implement file management with local disk storage provider. + ubeac-logo.jpg + https://github.com/ubeac/ubeac-api + + + + + + + + + + + diff --git a/src/FileManagement/uBeac.Core.Repositories.FileManagement.MongoDB/MongoFileRepository.cs b/src/FileManagement/uBeac.Core.Repositories.FileManagement.MongoDB/MongoFileRepository.cs new file mode 100644 index 00000000..dd2bf15c --- /dev/null +++ b/src/FileManagement/uBeac.Core.Repositories.FileManagement.MongoDB/MongoFileRepository.cs @@ -0,0 +1,45 @@ +using MongoDB.Driver; +using uBeac.Repositories.MongoDB; + +namespace uBeac.FileManagement.MongoDB; + +public class MongoFileRepository : MongoEntityRepository, IFileRepository + where TKey : IEquatable + where TEntity : IFileEntity + where TContext : IMongoDBContext +{ + public MongoFileRepository(TContext mongoDbContext, IApplicationContext applicationContext) : base(mongoDbContext, applicationContext) + { + } + + public async Task> Search(SearchFileRequest request, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + var filterBuilder = Builders.Filter; + var filters = FilterDefinition.Empty; + if (!string.IsNullOrWhiteSpace(request.Category)) filters &= filterBuilder.Eq(file => file.Category, request.Category); + if (request.Ids?.Any() == true) filters &= filterBuilder.In(file => file.Id, request.Ids); + if (request.Extensions?.Any() == true) filters &= filterBuilder.In(file => file.Extension, request.Extensions); + if (request.Providers?.Any() == true) filters &= filterBuilder.In(file => file.Provider, request.Providers); + + var result = await Collection.FindAsync(filters, new FindOptions(), cancellationToken); + return result.ToEnumerable(cancellationToken); + } +} + +public class MongoFileRepository : MongoFileRepository, IFileRepository + where TEntity : IFileEntity + where TContext : IMongoDBContext +{ + public MongoFileRepository(TContext mongoDbContext, IApplicationContext applicationContext) : base(mongoDbContext, applicationContext) + { + } +} + +public class MongoFileRepository : MongoFileRepository +{ + public MongoFileRepository(MongoDBContext mongoDbContext, IApplicationContext applicationContext) : base(mongoDbContext, applicationContext) + { + } +} \ No newline at end of file diff --git a/src/FileManagement/uBeac.Core.Repositories.FileManagement.MongoDB/uBeac.Core.FileManagement.Repositories.MongoDB.csproj b/src/FileManagement/uBeac.Core.Repositories.FileManagement.MongoDB/uBeac.Core.FileManagement.Repositories.MongoDB.csproj new file mode 100644 index 00000000..587c5055 --- /dev/null +++ b/src/FileManagement/uBeac.Core.Repositories.FileManagement.MongoDB/uBeac.Core.FileManagement.Repositories.MongoDB.csproj @@ -0,0 +1,19 @@ + + + + uBeac.FileManagement.Repositories.MongoDB + uBeac + net6.0 + enable + disable + Used to implement file management with MongoDB repository. + ubeac-logo.jpg + https://github.com/ubeac/ubeac-api + + + + + + + + diff --git a/src/FileManagement/uBeac.Core.Repositories.FileManagement.MongoDB/uBeac.Core.Repositories.FileManagement.MongoDB.csproj b/src/FileManagement/uBeac.Core.Repositories.FileManagement.MongoDB/uBeac.Core.Repositories.FileManagement.MongoDB.csproj deleted file mode 100644 index 220c9528..00000000 --- a/src/FileManagement/uBeac.Core.Repositories.FileManagement.MongoDB/uBeac.Core.Repositories.FileManagement.MongoDB.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net6.0 - enable - enable - - - - - - - - diff --git a/src/FileManagement/uBeac.Core.Services.FileManagement.Abstractions/uBeac.Core.Services.FileManagement.Abstractions.csproj b/src/FileManagement/uBeac.Core.Services.FileManagement.Abstractions/uBeac.Core.Services.FileManagement.Abstractions.csproj index 132c02c5..72554e93 100644 --- a/src/FileManagement/uBeac.Core.Services.FileManagement.Abstractions/uBeac.Core.Services.FileManagement.Abstractions.csproj +++ b/src/FileManagement/uBeac.Core.Services.FileManagement.Abstractions/uBeac.Core.Services.FileManagement.Abstractions.csproj @@ -6,4 +6,9 @@ enable + + + + + diff --git a/src/FileManagement/uBeac.Core.Providers.FileManagement.LocalStorage/uBeac.Core.Providers.FileManagement.LocalStorage.csproj b/src/FileManagement/uBeac.Core.Services.FileManagement/uBeac.Core.FileManagement.Services.csproj similarity index 100% rename from src/FileManagement/uBeac.Core.Providers.FileManagement.LocalStorage/uBeac.Core.Providers.FileManagement.LocalStorage.csproj rename to src/FileManagement/uBeac.Core.Services.FileManagement/uBeac.Core.FileManagement.Services.csproj diff --git a/src/FileManagement/uBeac.Core.Services.FileManagement/uBeac.Core.Services.FileManagement.csproj b/src/FileManagement/uBeac.Core.Services.FileManagement/uBeac.Core.Services.FileManagement.csproj deleted file mode 100644 index 132c02c5..00000000 --- a/src/FileManagement/uBeac.Core.Services.FileManagement/uBeac.Core.Services.FileManagement.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net6.0 - enable - enable - - - diff --git a/src/Identity/Tests/API/API.csproj b/src/Identity/Tests/API/API.csproj index 05fa98f3..530ee10a 100644 --- a/src/Identity/Tests/API/API.csproj +++ b/src/Identity/Tests/API/API.csproj @@ -13,6 +13,9 @@ + + + diff --git a/src/Identity/Tests/API/Avatars/t1zq2ubf.ncm b/src/Identity/Tests/API/Avatars/t1zq2ubf.ncm new file mode 100644 index 00000000..8f12a008 Binary files /dev/null and b/src/Identity/Tests/API/Avatars/t1zq2ubf.ncm differ diff --git a/src/Identity/Tests/API/Config/file-management.json b/src/Identity/Tests/API/Config/file-management.json new file mode 100644 index 00000000..69f5db3a --- /dev/null +++ b/src/Identity/Tests/API/Config/file-management.json @@ -0,0 +1,8 @@ +{ + "Avatars": { + "DirectoryPath": "Avatars" + }, + "Documents": { + "DirectoryPath": "Documents" + } +} \ No newline at end of file diff --git a/src/Identity/Tests/API/Controllers/AccountsController.cs b/src/Identity/Tests/API/Controllers/AccountsController.cs index 58da6e8f..afca1556 100644 --- a/src/Identity/Tests/API/Controllers/AccountsController.cs +++ b/src/Identity/Tests/API/Controllers/AccountsController.cs @@ -1,4 +1,6 @@ -namespace API; +using uBeac.FileManagement; + +namespace API; public class AccountsController : AccountsControllerBase { diff --git a/src/Identity/Tests/API/Controllers/AvatarsController.cs b/src/Identity/Tests/API/Controllers/AvatarsController.cs new file mode 100644 index 00000000..66ab22d0 --- /dev/null +++ b/src/Identity/Tests/API/Controllers/AvatarsController.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Mvc; +using uBeac.FileManagement; +using uBeac.Web; + +namespace API; + +public class AvatarsController : BaseController +{ + protected readonly IFileManager FileManager; + + public AvatarsController(IFileManager fileManager) + { + FileManager = fileManager; + } + + [HttpPost] + public async Task Upload([FromForm] IFormFile file, CancellationToken cancellationToken = default) + { + await using var stream = file.OpenReadStream(); + return await FileManager.Create(new FileModel + { + Stream = stream, + Category = "Avatars", + Extension = Path.GetExtension(file.FileName) + }, cancellationToken); + } + + [HttpPost] + public async Task Download([FromBody] GetFileRequest request, CancellationToken cancellationToken = default) + { + var response = await FileManager.Get(request, cancellationToken); + return new FileStreamResult(response.Stream, "application/octet-stream") { FileDownloadName = $"{request.Name}.{response.Extension}" }; + } + + [HttpPost] + public async Task> Search([FromBody] SearchFileRequest request, CancellationToken cancellationToken = default) + { + request.Category = "Avatars"; + return await FileManager.Search(request, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Identity/Tests/API/Program.cs b/src/Identity/Tests/API/Program.cs index 714a6186..dcb10af6 100644 --- a/src/Identity/Tests/API/Program.cs +++ b/src/Identity/Tests/API/Program.cs @@ -1,5 +1,7 @@ using System.Reflection; using Microsoft.AspNetCore.Mvc; +using uBeac.FileManagement.LocalStorage; +using uBeac.FileManagement.MongoDB; using uBeac.Repositories.History.MongoDB; using uBeac.Repositories.MongoDB; using uBeac.Web; @@ -11,9 +13,6 @@ // Adding json config files (IConfiguration) builder.Configuration.AddJsonConfig(builder.Environment); -// Adding bson serializers -//builder.Services.AddDefaultBsonSerializers(); - // Adding http logging builder.Services.AddMongoDbHttpLogging(() => { @@ -26,6 +25,24 @@ builder.Services.AddControllers(); builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly()); +builder.Services.AddScoped(); + +builder.Services.AddFileManagement(options => +{ + var avatarsLocalDiskOptions = builder.Configuration.GetInstance("Avatars"); + var documentsLocalDiskOptions = builder.Configuration.GetInstance("Documents"); + + options.AddCategory("Avatars") + .SetValidExtensions(new[] { ".png", ".jpg", ".jpeg", ".bmp" }) + .StoreInfoIn() + .StoreFilesIn(new LocalDiskFileProvider(avatarsLocalDiskOptions)); + + options.AddCategory("Documents") + .SetValidExtensions(new[] { ".docx", ".pdf" }) + .StoreInfoIn() + .StoreFilesIn(new LocalDiskFileProvider(documentsLocalDiskOptions)); +}); + // Disabling automatic model state validation builder.Services.Configure(options => { diff --git a/src/uBeac.Core.sln b/src/uBeac.Core.sln index 6a9dcea0..a41dcd38 100644 --- a/src/uBeac.Core.sln +++ b/src/uBeac.Core.sln @@ -55,17 +55,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "uBeac.Core.Web.Logging.Mong EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FileManagement", "FileManagement", "{0010CB43-FD6D-4C91-91D9-3065ACBAC2E1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "uBeac.Core.Services.FileManagement", "FileManagement\uBeac.Core.Services.FileManagement\uBeac.Core.Services.FileManagement.csproj", "{9020A29D-E4B8-42AE-9EE3-BC2B001A0CB9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "uBeac.Core.FileManagement.Repositories.MongoDB", "FileManagement\uBeac.Core.Repositories.FileManagement.MongoDB\uBeac.Core.FileManagement.Repositories.MongoDB.csproj", "{6B9F2368-1F55-43C0-9DDD-1CDE744F3118}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "uBeac.Core.Repositories.FileManagement.MongoDB", "FileManagement\uBeac.Core.Repositories.FileManagement.MongoDB\uBeac.Core.Repositories.FileManagement.MongoDB.csproj", "{6B9F2368-1F55-43C0-9DDD-1CDE744F3118}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "uBeac.Core.FileManagement.Providers.LocalDisk", "FileManagement\uBeac.Core.Providers.FileManagement.LocalDisk\uBeac.Core.FileManagement.Providers.LocalDisk.csproj", "{6780E437-7D94-4B08-9A25-CA6698C5D8DA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "uBeac.Core.Repositories.FileManagement.Abstractions", "FileManagement\uBeac.Core.Repositories.FileManagement.Abstractions\uBeac.Core.Repositories.FileManagement.Abstractions.csproj", "{6BAB5C0D-2624-43BC-B126-D40601B1E7A9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "uBeac.Core.FileManagement.Abstractions", "FileManagement\uBeac.Core.FileManagement.Abstractions\uBeac.Core.FileManagement.Abstractions.csproj", "{71C569AC-28C8-4268-81A2-964D7C48EBAC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "uBeac.Core.Providers.FileManagement.Abstractions", "FileManagement\uBeac.Core.Providers.FileManagement.Abstractions\uBeac.Core.Providers.FileManagement.Abstractions.csproj", "{D3461DFD-840B-4AB9-93C2-7B329FCF8536}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "uBeac.Core.FileManagement", "FileManagement\uBeac.Core.FileManagement\uBeac.Core.FileManagement.csproj", "{2D14DD45-130F-4B6B-B2CD-5F3F7AD28F96}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "uBeac.Core.Providers.FileManagement.LocalStorage", "FileManagement\uBeac.Core.Providers.FileManagement.LocalStorage\uBeac.Core.Providers.FileManagement.LocalStorage.csproj", "{6780E437-7D94-4B08-9A25-CA6698C5D8DA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "API", "Identity\Tests\API\API.csproj", "{04D4D6DE-2782-4A0D-9B74-1FF5141BE112}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "uBeac.Core.Services.FileManagement.Abstractions", "FileManagement\uBeac.Core.Services.FileManagement.Abstractions\uBeac.Core.Services.FileManagement.Abstractions.csproj", "{EA9BA74E-EE81-40E5-976A-EAB7C586F3D8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "uBeac.Core.FileManagement.Services", "FileManagement\uBeac.Core.FileManagement.Infrastructure\uBeac.Core.FileManagement.Services.csproj", "{50BE9416-E281-4FF1-BE56-B7CCC4A70869}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -149,30 +149,30 @@ Global {2D538398-EAC9-4A18-8459-74CA9E499F4E}.Debug|Any CPU.Build.0 = Debug|Any CPU {2D538398-EAC9-4A18-8459-74CA9E499F4E}.Release|Any CPU.ActiveCfg = Release|Any CPU {2D538398-EAC9-4A18-8459-74CA9E499F4E}.Release|Any CPU.Build.0 = Release|Any CPU - {9020A29D-E4B8-42AE-9EE3-BC2B001A0CB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9020A29D-E4B8-42AE-9EE3-BC2B001A0CB9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9020A29D-E4B8-42AE-9EE3-BC2B001A0CB9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9020A29D-E4B8-42AE-9EE3-BC2B001A0CB9}.Release|Any CPU.Build.0 = Release|Any CPU {6B9F2368-1F55-43C0-9DDD-1CDE744F3118}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6B9F2368-1F55-43C0-9DDD-1CDE744F3118}.Debug|Any CPU.Build.0 = Debug|Any CPU {6B9F2368-1F55-43C0-9DDD-1CDE744F3118}.Release|Any CPU.ActiveCfg = Release|Any CPU {6B9F2368-1F55-43C0-9DDD-1CDE744F3118}.Release|Any CPU.Build.0 = Release|Any CPU - {6BAB5C0D-2624-43BC-B126-D40601B1E7A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6BAB5C0D-2624-43BC-B126-D40601B1E7A9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6BAB5C0D-2624-43BC-B126-D40601B1E7A9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6BAB5C0D-2624-43BC-B126-D40601B1E7A9}.Release|Any CPU.Build.0 = Release|Any CPU - {D3461DFD-840B-4AB9-93C2-7B329FCF8536}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D3461DFD-840B-4AB9-93C2-7B329FCF8536}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D3461DFD-840B-4AB9-93C2-7B329FCF8536}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D3461DFD-840B-4AB9-93C2-7B329FCF8536}.Release|Any CPU.Build.0 = Release|Any CPU {6780E437-7D94-4B08-9A25-CA6698C5D8DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6780E437-7D94-4B08-9A25-CA6698C5D8DA}.Debug|Any CPU.Build.0 = Debug|Any CPU {6780E437-7D94-4B08-9A25-CA6698C5D8DA}.Release|Any CPU.ActiveCfg = Release|Any CPU {6780E437-7D94-4B08-9A25-CA6698C5D8DA}.Release|Any CPU.Build.0 = Release|Any CPU - {EA9BA74E-EE81-40E5-976A-EAB7C586F3D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EA9BA74E-EE81-40E5-976A-EAB7C586F3D8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA9BA74E-EE81-40E5-976A-EAB7C586F3D8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EA9BA74E-EE81-40E5-976A-EAB7C586F3D8}.Release|Any CPU.Build.0 = Release|Any CPU + {71C569AC-28C8-4268-81A2-964D7C48EBAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71C569AC-28C8-4268-81A2-964D7C48EBAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71C569AC-28C8-4268-81A2-964D7C48EBAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71C569AC-28C8-4268-81A2-964D7C48EBAC}.Release|Any CPU.Build.0 = Release|Any CPU + {2D14DD45-130F-4B6B-B2CD-5F3F7AD28F96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D14DD45-130F-4B6B-B2CD-5F3F7AD28F96}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D14DD45-130F-4B6B-B2CD-5F3F7AD28F96}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D14DD45-130F-4B6B-B2CD-5F3F7AD28F96}.Release|Any CPU.Build.0 = Release|Any CPU + {04D4D6DE-2782-4A0D-9B74-1FF5141BE112}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04D4D6DE-2782-4A0D-9B74-1FF5141BE112}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04D4D6DE-2782-4A0D-9B74-1FF5141BE112}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04D4D6DE-2782-4A0D-9B74-1FF5141BE112}.Release|Any CPU.Build.0 = Release|Any CPU + {50BE9416-E281-4FF1-BE56-B7CCC4A70869}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50BE9416-E281-4FF1-BE56-B7CCC4A70869}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50BE9416-E281-4FF1-BE56-B7CCC4A70869}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50BE9416-E281-4FF1-BE56-B7CCC4A70869}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -197,12 +197,11 @@ Global {AAA5B1BE-4216-4ADB-9FB3-B03D35CF8D03} = {2939E5A7-F1C3-4853-9F35-31EB64C08036} {CA77313D-6D97-4F91-9818-B729FA0359D2} = {2939E5A7-F1C3-4853-9F35-31EB64C08036} {2D538398-EAC9-4A18-8459-74CA9E499F4E} = {117FC7BE-0153-4819-9A27-0FDA22132FA1} - {9020A29D-E4B8-42AE-9EE3-BC2B001A0CB9} = {0010CB43-FD6D-4C91-91D9-3065ACBAC2E1} {6B9F2368-1F55-43C0-9DDD-1CDE744F3118} = {0010CB43-FD6D-4C91-91D9-3065ACBAC2E1} - {6BAB5C0D-2624-43BC-B126-D40601B1E7A9} = {0010CB43-FD6D-4C91-91D9-3065ACBAC2E1} - {D3461DFD-840B-4AB9-93C2-7B329FCF8536} = {0010CB43-FD6D-4C91-91D9-3065ACBAC2E1} {6780E437-7D94-4B08-9A25-CA6698C5D8DA} = {0010CB43-FD6D-4C91-91D9-3065ACBAC2E1} - {EA9BA74E-EE81-40E5-976A-EAB7C586F3D8} = {0010CB43-FD6D-4C91-91D9-3065ACBAC2E1} + {71C569AC-28C8-4268-81A2-964D7C48EBAC} = {0010CB43-FD6D-4C91-91D9-3065ACBAC2E1} + {2D14DD45-130F-4B6B-B2CD-5F3F7AD28F96} = {0010CB43-FD6D-4C91-91D9-3065ACBAC2E1} + {50BE9416-E281-4FF1-BE56-B7CCC4A70869} = {0010CB43-FD6D-4C91-91D9-3065ACBAC2E1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {247F9BF0-18BB-4A57-BB3E-2870DDAEF0D1}