How I migrated my Umbraco v7 site to v8 - Part 1 - Media

Posted written by Paul Seal on March 04, 2020 Umbraco

It all started for me on v7

I built this site in Umbraco v7 so I could learn more about Umbraco and blog about it at the same time.

Before v8 I'd only ever worked with v7, so I've never had the displeasure of using XSLT.

In the last year I have been working with v8 and at the end of 2019 I decided v8 was stable enough for me to move my blog over to it.

I didn't just want to upgrade to v8 though. I wanted to start with a fresh install of v8 and change properties etc and do things a bit different. You know what it's like when you get the chance to start something again, you remove the old bits that you don't like.

So that's where I was, I wanted to move my content and media over from my old site (approx 200 blog posts and 100 video posts) and put them into my new v8 site.

Part 1 - Migrating all of my old media

I usually get my header images from https://unsplash.com, it is a great resource for free images which don't require a licence or attribution.

I wanted to keep the paths to my media items all the same, and in v8 the media path naming system has changed. I needed to find a way to get all of my existing media into v8.

I created a simple POCO in my v7 project like this:

using System.Collections.Generic;

namespace CodeShare.Core.Models.Import
{
    public class MediaItem
    {
        public MediaItem()
        { }

        public string Type { get; set; }
        public string Name { get; set; }
        public int Id { get; set; }
        public string Key { get; set; }
        public string Url { get; set; }

        public List<MediaItem> Children { get; set; }
    }
}

And I created the item to hold a collection of them too:

using System.Collections.Generic;

namespace CodeShare.Core.Models.Import
{
    public class MediaItems
    {
        public MediaItems()
        { }

        public List<MediaItem> MediaItemsToImport { get; set; }
    }
}

Then I wrote some code to iterate through the media items in my v7 site, create a model representation of them using the classes above and then export them as xml.

I was lazy too, I didn't want to create a UmbracoApiController, so I just created a Template (View) and put the code in there. I used the altTemplate query string to use that view on the home page like this so that it fired the code. codeshare.localtest.me?altTemplate=ExportMedia

Here is the code I wrote in the view to export my media information to xml:

@inherits Umbraco.Web.Mvc.UmbracoTemplatePage

@using CodeShare.Core.Models.Import;
@using System.Linq;

@{

    Layout = null;
    var media = Umbraco.TypedMediaAtRoot();

    var mediaItemsModel = new MediaItems();

    mediaItemsModel.MediaItemsToImport = GetItemsToExport(media);

    System.Xml.Serialization.XmlSerializer writer =
        new System.Xml.Serialization.XmlSerializer(typeof(MediaItems));

    var path = @"D:\piess\Code\GitHub\codeshare\Import\Media.xml";
    System.IO.FileStream file = System.IO.File.Create(path);

    writer.Serialize(file, mediaItemsModel);
    file.Close();
}

@functions {
    private static List<MediaItem> GetItemsToExport(IEnumerable<IPublishedContent> mediaItems)
    {
        var itemsToExport = new List<MediaItem>();

        if (mediaItems != null &amp;&amp; mediaItems.Any())
        {
            foreach (var item in mediaItems)
            {
                var mediaItemToExport = new MediaItem
                {
                    Type = item.DocumentTypeAlias,
                    Name = item.Name,
                    Id = item.Id,
                    Key = item.GetKey().ToString(),
                    Url = item.Url
                };
                itemsToExport.Add(mediaItemToExport);
                if (item.Children != null &amp;&amp; item.Children.Any())
                {
                    mediaItemToExport.Children = GetItemsToExport(item.Children);
                }
            }
        }

        return itemsToExport;
    }
}

So now I had an xml file with content like this:

<?xml version="1.0"?>
<MediaItems xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <MediaItemsToImport>
    <MediaItem>
      <Type>Folder</Type>
      <Name>Blog</Name>
      <Id>1107</Id>
      <Key>2a2b40dc-72de-4660-91d7-13ca6281af0b</Key>
      <Url />
      <Children>
        <MediaItem>
          <Type>Folder</Type>
          <Name>Content Images</Name>
          <Id>1109</Id>
          <Key>c69981f8-63a4-4638-bb7b-17ccb15df7fc</Key>
          <Url />
          <Children>
            <MediaItem>
              <Type>Image</Type>
              <Name>image-file-name.png</Name>
              <Id>1110</Id>
              <Key>072210ad-c284-4db6-adc9-13b58229512b</Key>
              <Url>/media/1001/image-file-name.png</Url>
            </MediaItem>
          </Children>
        </MediaItem>
      </Children>
    </MediaItem>
  </MediaItemsToImport>
</MediaItems>

Getting the media items into v8

I copied the contents of the media folder in my v7 site and pasted it into the media folder on my fresh v8 site.

I copied the POCO classes above into the v8 project (that is why I used the namespace Import instead of Export).

Then in my v8 site, I used the same idea of the alt template to run the code in my view.

To make ?altTemplate= work in v8 I needed to add this code first and restart the site:

using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Web;
using Umbraco.Web.Routing;

namespace CodeShare.Core.Composing
{
    [RuntimeLevel(MinLevel = RuntimeLevel.Run)]
    public class UpdateContentFindersComposer : IUserComposer
    {
        public void Compose(Composition composition)
        {
            composition.ContentFinders().InsertBefore<ContentFinderByUrl, ContentFinderByUrlAndTemplate>();
        }
    }
}

Here is the code I put in my view called ImportMedia.cshtml

@using System.Xml.Serialization
@using CodeShare.Core.Models.Import
@using Newtonsoft.Json
@using Our.Umbraco.DocTypeGridEditor.Composing
@using Umbraco.Core.Logging
@using Umbraco.Core.PropertyEditors.ValueConverters
@using Umbraco.Core.Services
@using Current = Umbraco.Core.Composing.Current
@inherits Umbraco.Web.Mvc.UmbracoViewPage
@{
    Layout = null;

    XmlSerializer serializer =
        new XmlSerializer(typeof(MediaItems));

    // Declare an object variable of the type to be deserialized.
    MediaItems i;

    using (Stream reader = new FileStream(@"D:\piess\Code\GitHub\codeshare\Import\Media.xml", FileMode.Open))
    {
        // Call the Deserialize method to restore the object's state.
        i = (MediaItems)serializer.Deserialize(reader);
    }

    var mediaService = Current.Services.MediaService;
    var mediaTypeService = Current.Services.MediaTypeService;

    var item = i.MediaItemsToImport.FirstOrDefault();

    CreateMediaItems(-1, i.MediaItemsToImport, mediaService, mediaTypeService);
}

@functions
{

    private void CreateMediaItems(int parentId, List<MediaItem> mediaItems, IMediaService mediaService, IMediaTypeService mediaTypeService)
    {
        if (mediaItems != null &amp;&amp; mediaItems.Any())
        {
            foreach (var item in mediaItems)
            {
                var itemId = CreateMediaItem(mediaService, Current.Services.MediaTypeService, parentId, item.Type.ToLower(), new Guid(item.Key), item.Name, item.Url, false);
                CreateMediaItems(itemId, item.Children, mediaService, mediaTypeService);
            }
        }
    }


    private int CreateMediaItem(IMediaService service, IMediaTypeService mediaTypeService,
            int parentFolderId, string nodeTypeAlias, Guid key, string nodeName, string mediaPath, bool checkForDuplicateName = false)
    {
        if (service.GetById(key) != null)
            return -1;

        var mediaType = mediaTypeService.Get(nodeTypeAlias);
        if (mediaType == null)
        {
            return -1;
        }

        var isDuplicate = false;

        if (checkForDuplicateName)
        {
            IEnumerable<IMedia> children;
            if (parentFolderId == -1)
            {
                children = service.GetRootMedia();
            }
            else
            {
                var parentFolder = service.GetById(parentFolderId);
                if (parentFolder == null)
                {
                    return -1;
                }

                children = service.GetPagedChildren(parentFolderId, 0, int.MaxValue, out long totalRecords);
            }
            foreach (var m in children)
            {
                if (m.Name == nodeName)
                {
                    isDuplicate = true;
                    break;
                }
            }
        }

        if (isDuplicate) return -1;

        if (parentFolderId != -1)
        {
            var parentFolder = service.GetById(parentFolderId);
            if (parentFolder == null)
            {
                return -1;
            }
        }

        var media = service.CreateMedia(nodeName, parentFolderId, nodeTypeAlias);
        if (nodeTypeAlias == "Image")
            media.SetValue("umbracoFile", JsonConvert.SerializeObject(new ImageCropperValue { Src = mediaPath }));
        if (key != Guid.Empty)
        {
            media.Key = key;
        }
        service.Save(media);
        return media.Id;
    }
}

Successfully Imported

I was very excited, I had managed to import all of my media, they all had the same Key and file path as in the v7 site. I just had one further issue, they had no image crop value and weren't rendering on the front end. If I went into them individually and pressed save, they would get saved with an image cropper setting and would "heal" the crops, which would make them render on the front end. I needed a way to do this programmatically, so I wrote another view called healCrops.cshtml to loop through them all and heal the crops for them.

Healing the Image Cropper crops

Here is the code I use to heal the Image Cropper crop values:

@using Umbraco.Core.Composing
@using Umbraco.Core.Services
@inherits Umbraco.Web.Mvc.UmbracoViewPage
@{
    Layout = null;

    var mediaService = Current.Services.MediaService;
    var mediaAtRoot = mediaService.GetRootMedia();

    if (mediaAtRoot != null &amp;&amp; mediaAtRoot.Any())
    {
        HealCrops(mediaAtRoot, mediaService);
    }
}

@functions
{
    void HealCrops(IEnumerable<IMedia> mediaItems, IMediaService mediaService)
    {
        foreach (var item in mediaItems)
        {
            var mediaFromCache = Umbraco.Media(item.Id);

            if (item.ContentType.Alias == "Image")
            {
                var healedCrop = "{ \"src\": \"" + mediaFromCache.Url + "\", \"focalPoint\": { \"left\": 0.5, \"top\": 0.5 }, \"crops\": [ { \"alias\": \"Author Thumb\", \"width\": 40, \"height\": 40, \"coordinates\": null }, { \"alias\": \"Card Header\", \"width\": 510, \"height\": 290, \"coordinates\": null }, { \"alias\": \"List Square\", \"width\": 450, \"height\": 450, \"coordinates\": null }, { \"alias\": \"Page Header\", \"width\": 1904, \"height\": 604, \"coordinates\": null }, { \"alias\": \"Popular\", \"width\": 690, \"height\": 330, \"coordinates\": null }, { \"alias\": \"Author Tilted\", \"width\": 320, \"height\": 400, \"coordinates\": null } ] }";
                item.SetValue("umbracoFile", healedCrop);
                mediaService.Save(item);

                var itemChildren = mediaService.GetPagedChildren(item.Id, 0, 1000, out var totalRecords);
                if (itemChildren != null &amp;&amp; itemChildren.Any())
                {
                    HealCrops(itemChildren, mediaService);
                }
            }
        }
    }
}

As you can see, I got the media item from the cache, which meant I could get the url of the item and inject it into a json crop value setting that I took from an image which was working (I found the value from searching in the examine dashboard).

All set to move on.

Now that I had my media imported successfully, I could move on to importing the content. I will share this in part 2 of this blog post.