← Back to knowledge base |
  • Xperience by Kentico

Migrate Media Library items from Kentico CMS Portal Engine to Xperience by Kentico

In this article, you will learn how to migrate images stored in the Media Library and linked to a page from a Kentico CMS Portal Engine site to Xperience by Kentico.

This guide builds on my previous article about migrating text-based content, so it is highly recommended to read πŸ‘“ that first as a prerequisite. In code samples, I will share only the new additions.

Source code is alsoΒ available as a Github gist.

πŸ–ΌοΈ Adding Images to Our Mental Model

In our Portal Engine site, we have many News pages with linked images from a Media Library. In Xperience by Kentico, we store News page data in reusable content items, so we will add a field for the image in a similar but more robust fashion.

News page type in Portal engine - Image field addition

Our Data Transfer Approach

Our data transfer process includes the following steps:

  1. Serialize Data in Portal Engine: Convert the data, including the image URL, to JSON format.
  2. Migrate and Deserialize in Xperience by Kentico: Use a migration script to deserialize the JSON object, fetch the image data from the URL, store it in the Media Library, link it to a reusable content item, and then link that item to the News item.

Although creating a cascade of reusable content items might seem complex, it makes the content model more robust.

πŸ€– Serialize Image URL into JSON in Portal Engine

Using the Custom Response Repeater web part, which we download and import into our Portal Engine site admin, we create a new page dedicated to the JSON output. We then place and configure the Custom Response Repeater web part on this page.

Custom response repeater transformation with Image field addition

The critical addition happens in the transformation. We add an Image property to the JSON object, represented by an array of objects that store details about an image. This object includes the image Url, Name, and DocumentID, which we will use later in our migration script for unique naming. Including a property for the image alt description is advisable, but we'll exclude it in this example to stay focused on the migration job. Using an array supports scenarios where multiple images need migration.

...
"Image": [{
  "DocumentID": {% DocumentID %}, 
  "Name": "{% Replace(RegexReplace(Title, @"\t|\n|\r", ""), "\"", "\\\"") %} image",
  "Image": "{% ImageUrl %}",
}],
...

Side Note: For simplicity, in our Portal Engine site, the Image field uses the URL selector form control, storing the selected image URL in the database. If your setup uses a different form control storing a GUID, you'll need a transformation method to retrieve the file URL by the GUID identifier.

πŸ«— Import Image Data in Xperience by Kentico

Before tackling the migration script, ensure we have the necessary prerequisites from the previous article: the News parent page and content types for pages and reusable items. Additionally, create a Media Library and a content type for reusable content items to serve as a shell for image data.

The Media Library with a subfolder:

Media library UI

The Media Item content type for reusable content items:

Media item reusable content item type - Image field storing Media files

Field for News Content Item – Add a field where items based on the Media Item content type will reside:

News reusable content items type - Image field addition

πŸ‘ Extend the Migration Script

We will extend our existing solution by adding new code. Here's an overview of the process:

  1. Deserialize JSON News Items: Include the image in our Data Transfer Object model.
  2. Iterate and Map Data: When iterating through News items, include Media Library items. For each image:
    • Generate a unique code name identifier for the Media Item reusable content item.
    • Delete existing content items if repeating the migration run.
    • Migrate the Media Library file:
      1. Get reference for the Media Library object.
      2. Check if the file already exists.
      3. If not, download the image from the URL, create a MediaFile object, and return a reference.
    • Create a content item based on the Media Item content type and return references.
  3. Include References: Add the references in the News content item data.
  4. Create News Content Item and Page: Create the News content item, use its reference in the News page, and create the News page.

The solution consists of three files:

  • NewsDtoModel.cs – Extended with the media item model.
  • NewsMigrationController.cs – Now includes high-level additions for Media items migration in News pages.
  • MigrationHelper.cs – Implements logic for Media items and files migration, reusable in other scripts.

The extended NewsDtoModel.cs file

public class NewsItemDto
{
    ...
    public List<MediaItemDto> Image { get; set; }
}

public class MediaItemDto
{
    public int DocumentID { get; set; }
    public string Name { get; set; }
    public string Url { get; set; }
}

The extended NewsMigrationController.cs file

...

public class NewsMigrationController : Controller
{
    ...

    public NewsMigrationController(
        ...
    )
    {
        ...
    }

    [HttpPost("migration/news")]
    public async Task<IActionResult> Index()
    {
        ...

        foreach (var item in data)
        {
            ...
            var host = "https://<your host domain name>";
            var image = await migrationHelper.CreateMediaList(item.Image, "Images", "News/", host);

            ...

            var contentItemData = new ContentItemData(new Dictionary<string, object>{
                ...
                { "Image", image },
            });

            ...
        }
        return Ok();
    }
}

The extended MigrationHelper.cs file

...

public class MigrationHelper
{
    ...
    private readonly IInfoProvider<MediaLibraryInfo> mediaLibraryInfoProvider;
    private readonly IInfoProvider<MediaFileInfo> mediaFileInfoProvider;
    private readonly IMediaFileUrlRetriever mediaFileUrlRetriever;

    public MigrationHelper(
        ...
        IInfoProvider<MediaFileInfo> mediaFileInfoProvider,
        IInfoProvider<MediaLibraryInfo> mediaLibraryInfoProvider,
        IMediaFileUrlRetriever mediaFileUrlRetriever
    )
    {
        ...
        this.mediaFileInfoProvider = mediaFileInfoProvider;
        this.mediaLibraryInfoProvider = mediaLibraryInfoProvider;
        this.mediaFileUrlRetriever = mediaFileUrlRetriever;
    }

    ...

    /// <summary>
    /// Creates a media library file.
    /// </summary>
    /// <param name="remoteFileUrl">The URL of the remote file.</param>
    /// <param name="mediaLibraryName">The name of the media library.</param>
    /// <param name="mediaLibraryTargetFolder">The target folder in the media library.</param>
    /// <returns>The created media library file.</returns>
    public async Task<string> CreateMediaLibraryFile(string remoteFileUrl, string mediaLibraryName, string mediaLibraryTargetFolder)
    {
        // Get Media Library object reference
        MediaLibraryInfo library = mediaLibraryInfoProvider.Get(mediaLibraryName);

        if (library != null && !string.IsNullOrEmpty(remoteFileUrl))
        {
            // Get the file name from the URL
            string remoteFilePath = remoteFileUrl.Split("?").First();
            string fileName = remoteFilePath.Split("/").Last();

            // Check if the file already exists in the media library
            MediaFileInfo mediaFile = MediaFileInfoProvider.GetMediaFileInfo(library.LibraryID, $"{mediaLibraryTargetFolder}{fileName}");

            if (mediaFile == null)
            {
                // Get local path for the file
                string documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
                string filePath = Path.Combine(documentsPath, "Project", fileName);

                // Download the file to the local path
                using (var httpClient = new HttpClient())
                {
                    using (var response = await httpClient.GetAsync(remoteFilePath))
                    {
                        using (var fileStream = new FileStream(filePath, FileMode.Create))
                        {
                            await response.Content.CopyToAsync(fileStream);
                        }
                    }
                }

                // Create a new media file
                CMS.IO.FileInfo file = CMS.IO.FileInfo.New(filePath);

                if (file != null)
                {
                    mediaFile = new MediaFileInfo(filePath, library.LibraryID)
                    {
                        FileName = file.Name.Replace(file.Extension, ""),
                        FileTitle = "",
                        FileDescription = "",
                        FilePath = mediaLibraryTargetFolder,
                        FileExtension = file.Extension,
                        FileMimeType = MimeTypeHelper.GetMimetype(file.Extension),
                        FileLibraryID = library.LibraryID,
                        FileSize = file.Length
                    };

                    mediaFileInfoProvider.Set(mediaFile);
                }
            }

            // Return the media file reference
            if (mediaFile != null)
            {
                return $"[{{\"Identifier\":\"{mediaFile.FileGUID}\",\"Name\":\"{fileName}\",\"Size\":{mediaFile.FileSize},\"Dimensions\":{{\"Width\":{mediaFile.FileImageWidth},\"Height\":{mediaFile.FileImageHeight}}}}}]";
            }
        }

        return null;
    }

    /// <summary>
    /// Creates media list based on the media items.
    /// </summary>
    /// <param name="mediaItem">The media items.</param>
    /// <param name="mediaLibraryName">The name of the media library.</param>
    /// <param name="mediaLibraryTargetFolder">The target folder in the media library.</param>
    /// <param name="host">The host URL.</param>
    /// <returns>The list of media item references.</returns>
    public async Task<IEnumerable<ContentItemReference>> CreateMediaList(List<MediaItemMigration> mediaItems, string mediaLibraryName, string mediaLibraryTargetFolder, string host)
    {
        var mediaList = new List<ContentItemReference>();

        foreach (var item in mediaItems)
        {
            // Generate unique code name identifier for the reusable content item
            string codename = $"MigratedMediaItem{item.DocumentID}";

            // If item with such name already exists (when we repeat the migration), delete it
            await DeleteContentItemIfExists(MediaItemReusableContent.CONTENT_TYPE_NAME, codename);

            // Create media library file and get reference to it
            string image = await CreateMediaLibraryFile(item.Image.Replace("~", host), mediaLibraryName, mediaLibraryTargetFolder);

            // Create reusable content item
            CreateContentItemParameters createParams = new CreateContentItemParameters(
                MediaItemReusableContent.CONTENT_TYPE_NAME,
                codename,
                item.Name.Truncate(100),
                WebsiteConstants.LANGUAGE_DEFAULT
            );

            var itemData = new ContentItemData(new Dictionary<string, object>{
                { "Image", image }
            });

            var id = await contentItemManager.Create(createParams, itemData);
            await contentItemManager.TryPublish(id, WebsiteConstants.LANGUAGE_DEFAULT);

            // Get the created content item reference
            var builder = new ContentItemQueryBuilder();
            builder.ForContentType(MediaItemReusableContent.CONTENT_TYPE_NAME, subqueryConfiguration =>
            {
                subqueryConfiguration
                .TopN(1)
                .Where(where => where.WhereEquals("ContentItemID", id));
            });

            MediaItemReusableContent contentItem =
                (await contentQueryExecutor.GetMappedWebPageResult<MediaItemReusableContent>(builder)).FirstOrDefault();

            // Add the content item reference to the list
            mediaList.Add(new ContentItemReference
            {
                Identifier = contentItem.SystemFields.ContentItemGUID
            });
        }

        return mediaList;
    }
}

Find out more details and examples about the Media libraries API in the official documentation.

βš™οΈ Executing the Migration

Finally, run the site and initiate the migration script by making a POST request to the URL http://localhost:<port>/migration/news. Ensure you perform the migration on localhost to avoid timeouts; also the file migration process uses local storage when downloading the media files. Once finished, the Media files should be inside the Media Library, linked to reusable content items, and associated with the appropriate News reusable content items.

🏁 Final Words

Migrating Media files can be considered an additional layer on top of a general migration process, so I dedicated this article to it. Together with the previous article, I hope these guides provide a comprehensive approach to data migration, offering inspiration and ready-to-use solutions for your project.

About the author

Milan Lund is a Freelance Web Developer with Kentico Expertise. He specializes in building and maintaining websites in Xperience by Kentico. Milan writes articles based on his project experiences to assist both his future self and other developers.

Find out more
Milan Lund