Convert Media Type Programmatically

Vote:
 

Has anyone attempted to convert a media file type programmatically in EPiServer?

We have a type we modeled for images, ImageFile, and a type that was added via a plugin. We don't use the type added by the plugin but found that images uploaded will, from time to time, be created as the wrong type. I am hoping to figure out how to make the conversion so I can run a scheduled job to batch convert them all then remove the plugin.

So far, I can programmatically make a copy of A as type B then delete A.
What I would really like to do is convert A to B for a seamless transition.

First thing I tried is cloning the image as the proper type but that comes up null


var media = _contentRepository.Service.GetDescendents(SiteDefinition.Current.GlobalAssetsRoot)
.Where(i => _contentRepository.Service.Get<IContent>(i) is MediaData);
var imageFileTypeId = new ImageFile().ContentTypeID;
foreach (var img in media)
{
if (img.Get<IContent>() is GcEpiImageFile)
{
count++ // for summary
var item = _contentLoader.Service.Get<GcEpiImageFile>(img.Get<IContent>().ContentLink);
var cloneItem = item.CreateWritableClone() as ImageFile;
if (cloneItem == null)
{
var msg = $"{item.Name}: {item.ContentLink.ID} came up null";
OnStatusChanged(msg);
summaryMessage.AppendLine(msg);
continue;
}
cloneItem.ContentTypeID = imageFileTypeId;
_contentRepository.Service.Save(cloneItem, SaveAction.Publish, AccessLevel.NoAccess);

}

Next thing I tried is cloning it as the base class ImageData which seems to create the object but throws a Object reference not set to an instance of an object exception when saving.


var media = _contentRepository.Service.GetDescendents(SiteDefinition.Current.GlobalAssetsRoot)
.Where(i => _contentRepository.Service.Get<IContent>(i) is MediaData);
var imageFileTypeId = new ImageFile().ContentTypeID;
foreach (var img in media)
{
if (img.Get<IContent>() is GcEpiImageFile)
{
count++ // for summary
var item = _contentLoader.Service.Get<GcEpiImageFile>(img.Get<IContent>().ContentLink);
var cloneItem = item.CreateWritableClone() as ImageData;
if (cloneItem == null)
{
var msg = $"{item.Name}: {item.ContentLink.ID} came up null";
OnStatusChanged(msg);
summaryMessage.AppendLine(msg);
continue;
}
try
{
cloneItem.ContentTypeID = imageFileTypeId;
}
catch (Exception ex)
{
summaryMessage.AppendLine($"exception triggered by id assignment {ex.Message}<br />{ex.InnerException}");
}
try
{
_contentRepository.Service.Save(cloneItem, SaveAction.Publish, AccessLevel.NoAccess);
}
catch (Exception ex)
{
summaryMessage.AppendLine(
$"exception triggered by save {ex.Message}<br />{ex.InnerException}");
}
}


here is my stack trace:

Object reference not set to an instance of an object.
at EPiServer.Validation.Internal.RoutingSegmentValidator.Validate(IContent instance) at

EPiServer.Validation.Internal.ContextValidatorWrapper`2.Validate(Object instance, Object context)

at EPiServer.Validation.Internal.ValidationService.ValidateRecursively(Object instance, Object context, HashSet`1 visitedInstances)

at EPiServer.Validation.Internal.ValidationService.Validate[T](Object instance, T context)

at EPiServer.Core.ContentProvider.Validate(IContent content, ContentSaveValidationContext saveValidationContext)

at EPiServer.Core.Internal.DefaultContentRepository.Save(IContent content, SaveAction action, AccessLevel access) at Web.Business.ScheduledJobs.ImageConversion.Execute()


Any thoughts, help, guidance would be greatly appreciated

#221247
Edited, Apr 15, 2020 16:21
Vote:
 

I have solved pretty much the exact same problem using this SQL script. You need to modify it to fit your problem and accept there is a high amount of risk involved.

BEGIN TRANSACTION [ConvertTransaction]
BEGIN TRY
  DECLARE @FromPropertyID1 int
  DECLARE @ToPropertyID1 int
  DECLARE @FromPropertyID2 int
  DECLARE @ToPropertyID2 int
  DECLARE @FromPropertyID3 int
  DECLARE @ToPropertyID3 int
  
  DECLARE @RC int
  DECLARE @PageID int
  DECLARE @FromPageType int
  DECLARE @ToPageType int
  DECLARE @Recursive bit
  DECLARE @IsTest bit

  SELECT @PageID = 1
  SELECT @FromPageType = pkID FROM tblContentType WHERE ContentTypeGUID = '0a89e464-56d4-449f-aea8-2bf774ab8730'
  SELECT @ToPageType = pkID FROM tblContentType WHERE ContentTypeGUID = '40ed0421-2d37-4751-97dc-c7ea7033c3a5'
  
  SELECT @FromPropertyID1 = pkID FROM tblPropertyDefinition WHERE fkContentTypeID = @FromPageType AND [Name] = 'Copyright'
  SELECT @FromPropertyID2 = pkID FROM tblPropertyDefinition WHERE fkContentTypeID = @FromPageType AND [Name] = 'Description'
  SELECT @FromPropertyID3 = pkID FROM tblPropertyDefinition WHERE fkContentTypeID = @FromPageType AND [Name] = 'MediumThumbnail'
  
  SELECT @ToPropertyID1 = pkID FROM tblPropertyDefinition WHERE fkContentTypeID = @ToPageType AND [Name] = 'Copyright'
  SELECT @ToPropertyID2 = pkID FROM tblPropertyDefinition WHERE fkContentTypeID = @ToPageType AND [Name] = 'Description'
  SELECT @ToPropertyID3 = pkID FROM tblPropertyDefinition WHERE fkContentTypeID = @ToPageType AND [Name] = 'MediumThumbnail'
  
  SET @Recursive = 1
  SET @IsTest = 0
  
  print @PageID
  print @FromPageType
  print @ToPageType
  
  DECLARE @MasterLanguageID int

  SET @MasterLanguageID = 8
  
  EXECUTE @RC = [dbo].[netConvertPropertyForPageType]
     @PageID
    ,@FromPageType
    ,@FromPropertyID1
    ,@ToPropertyID1
    ,@Recursive
    ,@MasterLanguageID
    ,@IsTest

  print @RC
  
  EXECUTE @RC = [dbo].[netConvertPropertyForPageType]
     @PageID
    ,@FromPageType
    ,@FromPropertyID2
    ,@ToPropertyID2
    ,@Recursive
    ,@MasterLanguageID
    ,@IsTest

  print @RC

  EXECUTE @RC = [dbo].[netConvertPropertyForPageType]
     @PageID
    ,@FromPageType
    ,@FromPropertyID3
    ,@ToPropertyID3
    ,@Recursive
    ,@MasterLanguageID
    ,@IsTest

  print @RC
  
  EXECUTE @RC = [dbo].[netConvertPageType]
     @PageID
    ,@FromPageType
    ,@ToPageType
    ,@Recursive
    ,@IsTest
  
  print @RC

  COMMIT TRANSACTION [ConvertTransaction]
END TRY
BEGIN CATCH
  ROLLBACK TRANSACTION [ConvertTransaction]
END CATCH

-- Run separately
DECLARE @FromPageType int
SELECT @FromPageType = pkID FROM tblContentType WHERE ContentTypeGUID = '0a89e464-56d4-449f-aea8-2bf774ab8730'
EXECUTE netContentTypeDelete @ContentTypeID = @FromPageType
#221252
Apr 15, 2020 20:28
Matthew Potocnik - Apr 15, 2020 21:01
Jonah, 
I am REALLY hoping to avoid going the DB route if at all possible, especially since there are differing properties.
But, judging by all the spaghetti on my floor, it appears as though this may be my only option. 
Vote:
 

Has anyone had luck doing this using code only? 

I may find myself going the DB route but I am hoping to avoid that if possible.

#221260
Apr 15, 2020 21:37
Vote:
 

I think you can workaround by skipping the validation - it's probably not worth doing in this case. You can use 

SaveAction.Publish | SaveAction.SkipValidation 

#221284
Apr 16, 2020 7:08
Vote:
 

I had to do somehing similar for PDFS, my scheduled jon did utilise SQL as Johan suggested, the code is below and believe I found it somewhere in these forums 

    [ScheduledPlugIn(DisplayName = "Convert PDF Files", GUID = "d6619008-3e76-4886-b3c7-9a025a0c2603")]
    public class FixPdfMediaType : ScheduledJobBase
    {
        private readonly IContentRepository ContentRepository;
        private readonly IContentTypeRepository ContentTypeRepository;
        private readonly ContentMediaResolver MediaDataResolver;

        public FixPdfMediaType()
        {
            ContentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
            ContentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>();
            MediaDataResolver = ServiceLocator.Current.GetInstance<ContentMediaResolver>();
        }

        private List<BasicContent> GetPdfs()
        {
            var pdfs = new List<BasicContent>();
            var assets = ContentRepository.GetDescendents(SiteDefinition.Current.GlobalAssetsRoot);

            OnStatusChanged("Found " + assets.Count() + " assets");

            foreach (var asset in assets)
            {
                BasicContent pdf = null;
                if (ContentRepository.TryGet(asset, out pdf) && pdf.Name.EndsWith(".pdf"))
                {
                    pdfs.Add(pdf);
                }
            }

            return pdfs;
        }

        public override string Execute()
        {
            var pdfs = GetPdfs();

            OnStatusChanged("Handling " + pdfs.Count() + " PDF files");

            var output = new StringBuilder();
            int i = 0;

            //Get a suitable MediaData type from extension
            var mediaType = MediaDataResolver.GetFirstMatching(".pdf");
            var contentType = ContentTypeRepository.Load(mediaType);

            foreach (var pdf in pdfs)
            {
                if (pdf.ContentTypeID != contentType.ID)
                {
                    OnStatusChanged("Changing type for " + pdf.Name);

                    try
                    {
                        using (SqlConnection conn =
                                    new SqlConnection(ConfigurationManager.ConnectionStrings["EPiServerDB"].ConnectionString))
                        {
                            conn.Open();
                            using (SqlCommand cmd =
                                new SqlCommand("UPDATE tblPage SET fkPageTypeID=@ContentTypeID WHERE PkId = @ID", conn))
                            {
                                cmd.Parameters.AddWithValue("@ContentTypeID", contentType.ID);
                                cmd.Parameters.AddWithValue("@ID", pdf.ContentLink.ID);

                                cmd.ExecuteNonQuery();
                                i++;
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        output.AppendLine(string.Format("ID: {0}, Error: {1}",
                        pdf.ContentLink.ID,
                        ex.Message));
                    }
                }
            }

            return string.Format(
                "Fixed {0} pdfs. \n Errors:\n{1}",
                i,
                output);
        }
    }
}
#221287
Apr 16, 2020 7:26
Vote:
 

For the pages admin tool DefaultPageTypeConverter is used for coverting pages which is just IContent so very similar. If you look under the covers the following happens

private DataTable ConvertPageTypeProperties(
      int pageLinkId,
      int fromPageTypeId,
      List<KeyValuePair<int, int>> propertyTypeMap,
      bool recursive,
      bool isTest)
    {
      DataTable dataTable = new DataTable("Properties");
      dataTable.Locale = CultureInfo.InvariantCulture;
      dataTable.Columns.Add("FromPropertyID");
      dataTable.Columns.Add("ToPropertyID");
      dataTable.Columns.Add("Count");
      int id = this._languageBranchRepository.Load(this._contentRepository.Get<PageData>(new ContentReference(pageLinkId)).MasterLanguage).ID;
      foreach (KeyValuePair<int, int> propertyType in propertyTypeMap)
      {
        DbCommand command = this.CreateCommand("netConvertPropertyForPageType");
        command.Parameters.Add((object) this.CreateReturnParameter());
        command.Parameters.Add((object) this.CreateParameter("PageID", (object) pageLinkId));
        command.Parameters.Add((object) this.CreateParameter("FromPageType", (object) fromPageTypeId));
        command.Parameters.Add((object) this.CreateParameter("FromPropertyID", (object) propertyType.Key));
        command.Parameters.Add((object) this.CreateParameter("ToPropertyID", (object) propertyType.Value));
        command.Parameters.Add((object) this.CreateParameter("Recursive", (object) recursive));
        command.Parameters.Add((object) this.CreateParameter("MasterLanguageID", (object) id));
        command.Parameters.Add((object) this.CreateParameter("IsTest", (object) isTest));
        command.ExecuteNonQuery();
        DataRow row = dataTable.NewRow();
        row[0] = (object) propertyType.Key;
        row[1] = (object) propertyType.Value;
        row[2] = (object) this.GetReturnValue(command);
        dataTable.Rows.Add(row);
        if (this._propertyDefinitionRepository.Load(propertyType.Key).Type.DataType == PropertyDataType.Category)
        {
          command.CommandText = "netConvertCategoryPropertyForPageType";
          command.ExecuteNonQuery();
        }
      }
      return dataTable;
    }

    private DataTable ConvertPageType(
      int pageLinkId,
      int fromPageTypeId,
      int toPageTypeId,
      bool recursive,
      bool isTest)
    {
      DataTable dataTable = new DataTable("Pages");
      dataTable.Locale = CultureInfo.InvariantCulture;
      dataTable.Columns.Add("Count");
      DbCommand command = this.CreateCommand("netConvertPageType");
      command.Parameters.Add((object) this.CreateReturnParameter());
      command.Parameters.Add((object) this.CreateParameter("PageID", (object) pageLinkId));
      command.Parameters.Add((object) this.CreateParameter("FromPageType", (object) fromPageTypeId));
      command.Parameters.Add((object) this.CreateParameter("ToPageType", (object) toPageTypeId));
      command.Parameters.Add((object) this.CreateParameter("Recursive", (object) recursive));
      command.Parameters.Add((object) this.CreateParameter("IsTest", (object) isTest));
      command.ExecuteNonQuery();
      DataRow row = dataTable.NewRow();
      row["Count"] = (object) this.GetReturnValue(command);
      dataTable.Rows.Add(row);
      return dataTable;
    }

This suggest using the database is the correct approach. I would suggest seeing if you can alter this code/stored procedures to create a more generic converter helper. But using SQL such as how Johan has suggested would likely be the best approach.

#221330
Edited, Apr 16, 2020 11:31
Vote:
 

Testing out Johan's SQL I found the first part makes the conversion successfully but the second part fails with the following: 

The DELETE statement conflicted with the REFERENCE constraint "FK_tblWorkContentProperty_tblPropertyDefinition". The conflict occurred in table "dbo.tblWorkContentProperty", column 'fkPropertyDefinitionID'.

I am digging into it further but I am wondering if Johan, or anyone else who has tried something similar, ran into this issue.

#221518
Apr 20, 2020 14:01
- Apr 21, 2020 14:31
It sounds like you didn't match and convert all property definitions? I didn't see that error but my duplicates had the same definition names and setup.
Matthew Potocnik - Apr 21, 2020 14:41
That is exactly what happened.
I was actually able to fix it by reversing the process. Convert everything to type B. Convert back to type A. Delete B with no issue.

I appreciate you sharing your solution.
It has been incredibly helpful.
- Apr 21, 2020 14:49
Glad to help!

I wish Epi had some kind of prevention on startup so that a site cannot start if it has duplicate media types for a extension.
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.