Last active
December 1, 2017 06:55
-
-
Save mortenholmgaard/57fbe361ebe079bd1c97caf74de6a0cf to your computer and use it in GitHub Desktop.
Azure blob image provider for Episerver commerce product images
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private readonly ProductImageContentProvider _productImageContentProvider; | |
private void AddImageInEpiServerBackend(string colorProductCode, string imageName) | |
{ | |
var colorProductLink = _referenceConverterFactory.GetReferenceConverter().GetContentLinks(new []{ colorProductCode }).Values.FirstOrDefault(); | |
ColorProduct colorProduct = _contentRepository.Get<ColorProduct>(colorProductLink).CreateWritableClone() as ColorProduct; | |
ProductImageFile productImageFile = _productImageContentProvider.ConvertToProductImageFile(imageName); | |
colorProduct.CommerceMediaCollection.Add(new CommerceMedia | |
{ | |
AssetLink = productImageFile.ContentLink, | |
AssetType = "episerver.core.icontentimage", | |
GroupName = "default", | |
SortOrder = 0 | |
}); | |
_contentRepository.Save(colorProduct, SaveAction.Publish, AccessLevel.NoAccess); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The problem with the setting productImageFile.LargeThumbnail to a AzureBlob is that ValidateIdentifier() method in Blob is static and requires scheme to be "epi.fx.blob" | |
if (id.Scheme != "epi.fx.blob") | |
throw new ArgumentException("Scheme of a blob identifier must be epi.fx.blob", "id"); | |
Because it is static it is not possible to override it and it is called from EPiServer.SpecializedProperties.PropertyBlob so I can't override that either. | |
EPiServer.Core.InvalidPropertyValueException: "https://pompdeluxdevelopment.blob.core.windows.net/pompdelux/images/AW17/AikenYoSoftshellJacketAW17_DarkPurple_overview_01.png" is not a valid value for "LargeThumbnail". ---> System.ArgumentException: Scheme of a blob identifier must be epi.fx.blob | |
Parameter name: id | |
at EPiServer.Framework.Blobs.Blob.ValidateIdentifier(Uri id, Nullable`1 testForFile) | |
at EPiServer.SpecializedProperties.PropertyBlob.<>c__DisplayClass10_0.<set_Value>b__0() | |
at EPiServer.Core.PropertyData.SetPropertyValue(Object value, SetPropertyValueDelegate doSet) | |
--- End of inner exception stack trace --- | |
at EPiServer.Core.PropertyData.ThrowEditorFriendlyException(Object value, Exception e) | |
at EPiServer.Core.PropertyData.SetPropertyValue(Object value, SetPropertyValueDelegate doSet) | |
at EPiServer.DataAbstraction.RuntimeModel.ContentDataInterceptor.Intercept(IInvocation invocation) | |
at Castle.DynamicProxy.AbstractInvocation.Proceed() | |
at Vertica.Pompdelux.Business.Infrastructure.EPiServer.ProductImageContentProvider.ConvertToProductImageFile(String imageName) in C:\Code\POMPdeLUX\Development\src\Business\Infrastructure\EPiServer\ProductImageContentProvider.cs:line 101 | |
at Vertica.Pompdelux.Website.Jobs.ImportProductImagesJob.AddImageInEpiServerBackend(String colorProductCode, String imageName) in C:\Code\POMPdeLUX\Development\src\Website\Jobs\ImportProductImagesJob.cs:line 337 | |
at Vertica.Pompdelux.Website.Jobs.ImportProductImagesJob.Execute() in C:\Code\POMPdeLUX\Development\src\Website\Jobs\ImportProductImagesJob.cs:line 170 | |
And the Component does not work either - when accessing episerver/cms the main area goes all white. | |
I get this error in the EPiServerErrorWarn.log: WARN EPiServer.Shell.Navigation.MenuAssembler: Could not find the parent path [/global/find] | |
The code is based on this guide: http://world.episerver.com/blogs/Per-Magne-Skuseth/Dates/2014/11/content-providers-101--part-i-introduction-initialization-ui--identity-mapping/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Vertica.Pompdelux.Website.Infrastructure.Boostrapping | |
{ | |
[InitializableModule] | |
[ModuleDependency(typeof(EPiServer.Commerce.Initialization.InitializationModule))] | |
public class InitializationModule : IConfigurableModule | |
{ | |
public void Initialize(InitializationEngine context) | |
{ | |
... | |
ConfigureProductImagesProvider(context); | |
} | |
private static void ConfigureProductImagesProvider(InitializationEngine context) | |
{ | |
var providerValues = new NameValueCollection | |
{ | |
{ | |
ContentProviderElement.EntryPointString, ProductImageContentProvider.GetEntryPoint(ProductImageContentProvider.Key).ContentLink.ToString() | |
} | |
}; | |
var productImageContentProvider = context.Locate.Advanced.GetInstance<ProductImageContentProvider>(); | |
productImageContentProvider.Initialize(ProductImageContentProvider.Key, providerValues); | |
var providerManager = context.Locate.Advanced.GetInstance<IContentProviderManager>(); | |
providerManager.ProviderMap.AddProvider(productImageContentProvider); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using EPiServer.Shell; | |
using EPiServer.Shell.ViewComposition; | |
namespace Vertica.Pompdelux.Business.Infrastructure.EPiServer | |
{ | |
[Component] | |
public class ProductImageComponent : ComponentDefinitionBase | |
{ | |
public ProductImageComponent() : base("epi-cms.component.Media") | |
{ | |
Categories = new[] { "content" }; | |
Title = "Product images"; | |
Description = "All product images from azure blob storage"; | |
SortOrder = 1000; | |
PlugInAreas = new[] { PlugInArea.AssetsDefaultGroup, "/episerver/commerce/assets/defaultgroup" }; | |
Settings.Add(new Setting("repositoryKey", ProductImageContentProvider.Key)); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Collections.Specialized; | |
using System.Linq; | |
using System.Text; | |
using EPiServer; | |
using EPiServer.Azure.Blobs; | |
using EPiServer.Core; | |
using EPiServer.DataAbstraction; | |
using EPiServer.DataAccess; | |
using EPiServer.Security; | |
using EPiServer.ServiceLocation; | |
using EPiServer.Web.Routing; | |
using Vertica.Pompdelux.Business.Infrastructure.Blob; | |
using Vertica.Pompdelux.Shared.Cache; | |
using Vertica.Pompdelux.Shared.Models.Media; | |
namespace Vertica.Pompdelux.Business.Infrastructure.EPiServer | |
{ | |
/// <summary> | |
/// https://world.episerver.com/blogs/Per-Magne-Skuseth/Dates/2014/11/content-providers-101--part-i-introduction-initialization-ui--identity-mapping/ | |
/// http://world.episerver.com/forum/developer-forum/EPiServer-Commerce/Thread-Container/2014/9/Uploading-CommerceMedia-in-assets-programatically/?pageIndex=1 | |
/// </summary> | |
public class ProductImageContentProvider : ContentProvider | |
{ | |
private readonly IContentRepository _contentRepository; | |
private readonly CustomAzureBlobProvider _produtImagesAzureBlobProvider; | |
private readonly IBlobService _blobService; | |
private readonly ICacheProvider _cacheProvider; | |
private const string AllProductImageUrlsCacheKey = "AllProductImageUrls"; | |
public ProductImageContentProvider(IContentRepository contentRepository, IBlobService blobService, ICacheProvider cacheProvider) | |
{ | |
_contentRepository = contentRepository; | |
_blobService = blobService; | |
_cacheProvider = cacheProvider; | |
_produtImagesAzureBlobProvider = new CustomAzureBlobProvider(); | |
_produtImagesAzureBlobProvider.Initialize("productsinazureblobs", new NameValueCollection { { "connectionStringName", "EPiServerAzureBlobs" }, { "container", "pompdelux" } }); | |
} | |
public const string Key = "productimages"; | |
protected Injected<IdentityMappingService> IdentityMappingService { get; set; } | |
protected override IContent LoadContent(ContentReference contentLink, ILanguageSelector languageSelector) | |
{ | |
MappedIdentity mappedIdentity = IdentityMappingService.Service.Get(contentLink); | |
// The imageName is found in the ExternalIdentifier that was created earlier. Note that Segments[1] is used due to the fact that the ExternalIdentifier is of type Uri. | |
// It contains two segments. Segments[0] contains the content provider key, and Segments[1] contains the unique path, which is the imageName in this case. | |
string imageName = mappedIdentity.ExternalIdentifier.Segments[1]; | |
var productImageFile = ConvertToProductImageFile(imageName); | |
//AddContentToCache(productImageFile); //TODO properly add | |
return productImageFile; | |
} | |
/// <summary> | |
/// NOTE: productImageUrls is cached in one day | |
/// This will pass back content reference for all the children for a specific node. In this case, it will be a flat structure, | |
/// so this will only be loaded with the provider's EntryPoint set as the contentLink | |
/// </summary> | |
protected override IList<GetChildrenReferenceResult> LoadChildrenReferencesAndTypes( | |
ContentReference contentLink, string languageID, out bool languageSpecific) | |
{ | |
languageSpecific = false; | |
string[] productImageUrls; | |
if (!_cacheProvider.TryGet(AllProductImageUrlsCacheKey, out productImageUrls)) | |
{ | |
productImageUrls = _blobService.GetAllProductImageUrls(); | |
_cacheProvider.Put(AllProductImageUrlsCacheKey, productImageUrls, CacheProvider.OneDay); | |
} | |
// create and return GetChildrenReferenceResults. The ContentReference (ContentLink) is fetched using the IdentityMapingService. | |
return productImageUrls.Select(x => | |
new GetChildrenReferenceResult | |
{ | |
ContentLink = IdentityMappingService.Service.Get(MappedIdentity.ConstructExternalIdentifier(ProviderKey, x)).ContentLink, | |
ModelType = typeof(ProductImageFile) | |
}).ToList(); | |
} | |
protected override Uri ConstructContentUri(int contentTypeId, ContentReference contentLink, Guid contentGuid) | |
{ | |
return base.ConstructContentUri(contentTypeId, contentLink, contentGuid); | |
} | |
protected override IList<MatchingSegmentResult> ListMatchingSegments(ContentReference parentLink, string urlSegment) | |
{ | |
//return base.ListMatchingSegments(parentLink, urlSegment); | |
var list = new List<MatchingSegmentResult>(); | |
foreach (var child in LoadChildren<IContent>(parentLink, LanguageSelector.Fallback("da-DK", true), -1, -1)) | |
{ | |
var routable = child as IRoutable; | |
var isMatch = routable != null && urlSegment.Equals(routable.RouteSegment, StringComparison.OrdinalIgnoreCase); | |
if (isMatch) | |
{ | |
list.Add(new MatchingSegmentResult | |
{ | |
ContentLink = child.ContentLink | |
}); | |
} | |
} | |
return list; | |
} | |
public ProductImageFile ConvertToProductImageFile(string imageName) | |
{ | |
ProductImageFile productImageFile = _contentRepository.GetDefault<ProductImageFile>(DataFactory.Instance.Get<ContentFolder>(EntryPoint).ContentLink); | |
productImageFile.Status = VersionStatus.Published; | |
productImageFile.IsPendingPublish = false; | |
productImageFile.StartPublish = DateTime.Now.Subtract(TimeSpan.FromDays(14)); | |
// This part is a bit tricky. IdentityMappingService is used in order to create the ContentReference and content GUID. Creates an external identifier based on the imageName | |
Uri externalId = MappedIdentity.ConstructExternalIdentifier(ProviderKey, imageName); | |
// Make sure Get is invoked with the second parameter ('createMissingMapping') set to true. This will create a new mapping if no existing mapping is found | |
MappedIdentity mappedContent = IdentityMappingService.Service.Get(externalId, true); | |
productImageFile.ContentLink = mappedContent.ContentLink; | |
productImageFile.ContentGuid = mappedContent.ContentGuid; | |
var blobPath = $"epi.fx.blob://productimages/images/{GetSeason(imageName)}/{imageName}"; | |
var blob = _produtImagesAzureBlobProvider.GetBlob(new Uri(MakeValidBlobPathForEpiServerValidation(blobPath))); | |
productImageFile.Name = imageName; | |
productImageFile.BinaryData = blob; | |
productImageFile.Thumbnail = blob; | |
productImageFile.LargeThumbnail = blob; | |
productImageFile.MakeReadOnly(); | |
return productImageFile; | |
} | |
public static ContentFolder GetEntryPoint(string name) | |
{ | |
var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>(); | |
var folder = contentRepository.GetBySegment(ContentReference.RootPage, name, LanguageSelector.AutoDetect()) as ContentFolder; | |
if (folder == null) | |
{ | |
folder = contentRepository.GetDefault<ContentFolder>(ContentReference.RootPage); | |
folder.Name = name; | |
contentRepository.Save(folder, SaveAction.Publish, AccessLevel.NoAccess); | |
} | |
return folder; | |
} | |
/// <summary> | |
/// Make a url to circumvent Blob method: ValidateIdentifier(Uri id, bool? testForFile) | |
/// Is changed back in CustomAzureBlobContainer | |
/// E.g. "epi.fx.blob://productimages/images/AW17/imageName.png" ==> "epi.fx.blob://productimages/images/AW17|imageName.png" | |
/// </summary> | |
/// <returns>Valid Episerver blob url</returns> | |
private string MakeValidBlobPathForEpiServerValidation(string url) | |
{ | |
if (url.StartsWith("epi.fx.blob://default")) | |
return url; | |
const int numberOfSlashes = 4; | |
var builder = new StringBuilder(); | |
var urlParts = url.Split('/'); | |
for (var i = 0; i < urlParts.Length; i++) | |
{ | |
if (i > 0) | |
builder.Append(i > numberOfSlashes ? "|" : "/"); | |
builder.Append(urlParts[i]); | |
} | |
url = builder.ToString(); | |
return url; | |
} | |
public static string GetSeason(string imageName) | |
{ | |
var styleCode = imageName.Split('_').First(); | |
return styleCode.Substring(styleCode.Length - 4, 4); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.ComponentModel.DataAnnotations; | |
using EPiServer.Commerce.SpecializedProperties; | |
using EPiServer.DataAnnotations; | |
namespace Vertica.Pompdelux.Shared.Models.Media | |
{ | |
[AdministrationSettings( | |
CodeOnly = true, | |
GroupName = "ProductImages")] | |
[ContentType(GUID = "5CF67A99-CB6D-4DFB-9EB7-7692B5F14187", | |
DisplayName = "Product image", | |
GroupName = "ProductImages")] | |
public class ProductImageFile : CommerceImage | |
{ | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using EPiServer.Core; | |
using EPiServer.ServiceLocation; | |
using EPiServer.Shell; | |
using Vertica.Pompdelux.Shared.Models.Media; | |
namespace Vertica.Pompdelux.Business.Infrastructure.EPiServer | |
{ | |
[ServiceConfiguration(typeof(IContentRepositoryDescriptor))] | |
public class ProductImageRepositoryDescriptor : ContentRepositoryDescriptorBase | |
{ | |
protected Injected<IContentProviderManager> ContentProviderManager { get; set; } | |
public override string Key => ProductImageContentProvider.Key; | |
public override string Name => "Product images"; | |
public override IEnumerable<ContentReference> Roots => new[] { ContentProviderManager.Service.GetProvider(ProductImageContentProvider.Key).EntryPoint }; | |
public override IEnumerable<Type> ContainedTypes => new[] { typeof(ProductImageFile) }; | |
public override IEnumerable<Type> MainNavigationTypes => new[] { typeof(ContentFolder) }; | |
public override IEnumerable<Type> CreatableTypes => new[] { typeof(ProductImageFile) }; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment