- 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.
Our Data Transfer Approach
Our data transfer process includes the following steps:
- Serialize Data in Portal Engine: Convert the data, including the image URL, to JSON format.
- 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.
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:
The Media Item content type for reusable content items:
Field for News Content Item β Add a field where items based on the Media Item content type will reside:
π Extend the Migration Script
We will extend our existing solution by adding new code. Here's an overview of the process:
- Deserialize JSON News Items: Include the image in our Data Transfer Object model.
- 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:
- Get reference for the Media Library object.
- Check if the file already exists.
- 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.
- Include References: Add the references in the News content item data.
- 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