Skip to content

Commit 9c53d56

Browse files
authored
Merge pull request #505 from Kentico/fix/media-folders
Fix legacy media folders in multiworkspace scenarios
2 parents 08e80c5 + e407728 commit 9c53d56

File tree

3 files changed

+51
-38
lines changed

3 files changed

+51
-38
lines changed

KVA/Migration.Tool.Source/Services/AssetFacade.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Kentico.Xperience.UMT.Services;
1111
using Microsoft.Extensions.Logging;
1212
using Migration.Tool.Common;
13+
using Migration.Tool.Common.ContentItemOptions;
1314
using Migration.Tool.Common.Helpers;
1415
using Migration.Tool.Common.MigrationProtocol;
1516
using Migration.Tool.Common.Services;
@@ -31,9 +32,10 @@ public interface IAssetFacade
3132
/// <param name="site">CmsSite that owns media file in source instance</param>
3233
/// <param name="contentLanguageNames">preferably only default language</param>
3334
/// <param name="workspaceGuid">Optional workspace GUID for folder creation. If null, uses fallback workspace logic</param>
34-
/// <returns></returns>
35+
/// <param name="contentFolderOptions">Optional explicit specification of content folder in which to put the content item. If null, source folder structure is reflected</param>
36+
/// <returns>UMT model to import</returns>
3537
/// <exception cref="InvalidOperationException">occurs when media path cannot be determined</exception>
36-
Task<ContentItemSimplifiedModel> FromMediaFile(IMediaFile mediaFile, IMediaLibrary mediaLibrary, ICmsSite site, string[] contentLanguageNames, Guid? workspaceGuid = null);
38+
Task<ContentItemSimplifiedModel> FromMediaFile(IMediaFile mediaFile, IMediaLibrary mediaLibrary, ICmsSite site, string[] contentLanguageNames, Guid? workspaceGuid = null, ContentFolderOptions? contentFolderOptions = null);
3739

3840
/// <summary>
3941
/// Translates legacy attachment to new preferred storage - content item
@@ -92,7 +94,7 @@ public string DefaultContentLanguage
9294
}
9395

9496
/// <inheritdoc />
95-
public async Task<ContentItemSimplifiedModel> FromMediaFile(IMediaFile mediaFile, IMediaLibrary mediaLibrary, ICmsSite site, string[] contentLanguageNames, Guid? workspaceGuid = null)
97+
public async Task<ContentItemSimplifiedModel> FromMediaFile(IMediaFile mediaFile, IMediaLibrary mediaLibrary, ICmsSite site, string[] contentLanguageNames, Guid? workspaceGuid = null, ContentFolderOptions? contentFolderOptions = null)
9698
{
9799
Debug.Assert(mediaFile.FileLibraryID == mediaLibrary.LibraryID, "mediaFile.FileLibraryID == mediaLibrary.LibraryID");
98100
Debug.Assert(mediaLibrary.LibrarySiteID == site.SiteID, "mediaLibrary.LibrarySiteID == site.SiteID");
@@ -159,15 +161,15 @@ public async Task<ContentItemSimplifiedModel> FromMediaFile(IMediaFile mediaFile
159161

160162
string mediaFolder = Path.Combine(mediaLibrary.LibraryFolder, Path.GetDirectoryName(mediaFile.FilePath)!);
161163

162-
var folderGuid = await EnsureMediaFolder(mediaFolder, site, workspaceGuid);
164+
var folderGuid = contentFolderOptions is not null ? contentFolderService.EnsureFolder(contentFolderOptions, true, workspaceGuid) : await EnsureMediaFolder(mediaFolder, site, workspaceGuid);
163165

164166
string? contentItemSafeName = await Service.Resolve<IContentItemCodeNameProvider>().Get($"{mediaFile.FileName}_{translatedMediaGuid}");
165167
var contentItem = new ContentItemSimplifiedModel
166168
{
167169
CustomProperties = [],
168170
ContentItemGUID = translatedMediaGuid,
169171
ContentItemContentFolderGUID = folderGuid,
170-
ContentItemWorkspaceGUID = workspaceService.FallbackWorkspace.Value.WorkspaceGUID,
172+
ContentItemWorkspaceGUID = workspaceGuid,
171173
IsSecured = null,
172174
ContentTypeName = LegacyMediaFileContentType.ClassName,
173175
Name = contentItemSafeName,
@@ -242,6 +244,7 @@ public async Task<ContentItemSimplifiedModel> FromAttachment(ICmsAttachment atta
242244

243245
private async Task<Guid?> EnsureMediaFolder(string sourceFolderFilesystemPath, ICmsSite site, Guid? workspaceGuid = null)
244246
{
247+
workspaceGuid ??= workspaceService.FallbackWorkspace.Value.WorkspaceGUID;
245248
string folderSubPath = sourceFolderFilesystemPath.Replace(Path.DirectorySeparatorChar, '/');
246249

247250
var pathTemplate = new List<(Guid Guid, string Name, string DisplayName, string PathSegmentName)>();
@@ -265,7 +268,7 @@ public async Task<ContentItemSimplifiedModel> FromAttachment(ICmsAttachment atta
265268

266269
string absolutePath = $"{rootPath.TrimEnd('/')}/{folderSubPath.TrimStart('/')}";
267270

268-
pathTemplate.AddRange(ContentFolderService.StandardPathTemplate(site.SiteName, absolutePath));
271+
pathTemplate.AddRange(ContentFolderService.StandardPathTemplate(site.SiteName, absolutePath, workspaceGuid!.Value));
269272
}
270273

271274
return await contentFolderService.EnsureFolderStructure(pathTemplate, workspaceGuid);

KVA/Migration.Tool.Source/Services/MediaFileMigratorToContentItem.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,9 @@ private async Task MigrateToAssets()
5555
var directive = GetDirective(new(ksSite, ksMediaLibrary, ksMediaFile));
5656

5757
var workspaceGuid = workspaceService.EnsureWorkspace(directive.WorkspaceOptions);
58-
var umtContentItem = await assetFacade.FromMediaFile(ksMediaFile, ksMediaLibrary, ksSite, [defaultContentLanguage.ContentLanguageName], workspaceGuid);
58+
var umtContentItem = await assetFacade.FromMediaFile(ksMediaFile, ksMediaLibrary, ksSite, [defaultContentLanguage.ContentLanguageName], workspaceGuid, directive.ContentFolderOptions);
5959

6060
umtContentItem.ContentItemWorkspaceGUID = workspaceGuid;
61-
umtContentItem.ContentItemContentFolderGUID = contentFolderService.EnsureFolder(directive.ContentFolderOptions, true, workspaceGuid);
6261

6362
foreach (var item in umtContentItem.LanguageData)
6463
{

Migration.Tool.Common/Services/ContentFolderService.cs

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class ContentFolderService(IImporter importer, ILogger<ContentFolderServi
1212
/// <summary>
1313
/// Folder tree path as key
1414
/// </summary>
15-
private readonly Dictionary<string, Guid> folderGuidCache = [];
15+
private readonly Dictionary<string, ContentFolderInfo> folderCache = [];
1616

1717
/// <summary>
1818
/// Iterates over the folder path. If a folder doesn't exist, it gets created
@@ -22,23 +22,27 @@ public class ContentFolderService(IImporter importer, ILogger<ContentFolderServi
2222
/// <exception cref="InvalidOperationException"></exception>
2323
public async Task<Guid?> EnsureFolderStructure(IEnumerable<(Guid Guid, string Name, string DisplayName, string PathSegmentName)> folderPathTemplate, Guid? workspaceGuid = null)
2424
{
25-
workspaceGuid ??= workspaceService.FallbackWorkspace.Value.WorkspaceGUID;
26-
Guid? parentGuid = GetWorkspaceRootFolder(workspaceGuid.Value);
25+
var workspaceInfo = workspaceGuid is not null
26+
? (workspaceService.GetWorkspace(workspaceGuid!.Value) ?? workspaceService.FallbackWorkspace.Value)
27+
: workspaceService.FallbackWorkspace.Value;
2728

28-
var currentPath = string.Empty;
29+
ContentFolderInfo? parentFolderInfo = GetWorkspaceRootFolder(workspaceInfo.WorkspaceGUID);
30+
31+
string currentPath = string.Empty;
2932
foreach (var folderTemplate in folderPathTemplate)
3033
{
31-
Guid newParentGuid;
34+
ContentFolderInfo newParentFolder;
3235
currentPath += $"/{folderTemplate.PathSegmentName}";
33-
if (folderGuidCache.TryGetValue(currentPath.ToString(), out var folderGuid))
36+
var folderCacheKey = $"{workspaceInfo.WorkspaceGUID}|{currentPath}";
37+
if (folderCache.TryGetValue(folderCacheKey, out var folderGuid))
3438
{
35-
newParentGuid = folderGuid;
39+
newParentFolder = folderGuid;
3640
}
3741
else
3842
{
3943
var folderInfo = ContentFolderInfo.Provider.Get()
40-
.WhereEquals(nameof(ContentFolderInfo.ContentFolderGUID), folderTemplate.Guid)
4144
.And().WhereEquals(nameof(ContentFolderInfo.ContentFolderDisplayName), folderTemplate.DisplayName)
45+
.And().WhereEquals(nameof(ContentFolderInfo.ContentFolderWorkspaceID), workspaceInfo.WorkspaceID)
4246
.FirstOrDefault();
4347

4448
if (folderInfo is null)
@@ -49,15 +53,15 @@ public class ContentFolderService(IImporter importer, ILogger<ContentFolderServi
4953
ContentFolderName = UniqueNameHelper.MakeUnique(folderTemplate.Name, x => !ContentFolderInfo.Provider.Get().WhereEquals(nameof(ContentFolderInfo.ContentFolderName), x).Any()),
5054
ContentFolderDisplayName = folderTemplate.DisplayName,
5155
ContentFolderTreePath = currentPath,
52-
ContentFolderParentFolderGUID = parentGuid,
53-
ContentFolderWorkspaceGUID = workspaceGuid
56+
ContentFolderParentFolderGUID = parentFolderInfo.ContentFolderGUID,
57+
ContentFolderWorkspaceGUID = workspaceInfo.WorkspaceGUID
5458
};
5559

5660
switch (await importer.ImportAsync(newFolderModel))
5761
{
58-
case { Success: true }:
62+
case { Success: true, Imported: ContentFolderInfo importedInfo }:
5963
{
60-
newParentGuid = folderGuidCache[currentPath] = folderTemplate.Guid;
64+
newParentFolder = folderCache[folderCacheKey] = importedInfo;
6165
break;
6266
}
6367
case { Success: false, Exception: { } exception }:
@@ -81,35 +85,42 @@ public class ContentFolderService(IImporter importer, ILogger<ContentFolderServi
8185
}
8286
else
8387
{
84-
newParentGuid = folderInfo.ContentFolderGUID;
88+
// The following inconsistency may exist in database due to migrations by previous versions of MT. If so, we patch it.
89+
if (folderInfo.ContentFolderParentFolderID != parentFolderInfo.ContentFolderID)
90+
{
91+
folderInfo.ContentFolderParentFolderID = parentFolderInfo.ContentFolderID;
92+
ContentFolderInfo.Provider.Set(folderInfo);
93+
}
94+
95+
newParentFolder = folderInfo;
8596
}
8697
}
8798

88-
parentGuid = newParentGuid;
99+
parentFolderInfo = newParentFolder;
89100
}
90101

91-
return parentGuid;
102+
return parentFolderInfo.ContentFolderGUID;
92103
}
93104

94-
private readonly Dictionary<Guid, Guid> workspaceRootFolderCache = [];
95-
public Guid GetWorkspaceRootFolder(Guid workspaceGuid)
105+
private readonly Dictionary<Guid, ContentFolderInfo> workspaceRootFolderCache = [];
106+
public ContentFolderInfo GetWorkspaceRootFolder(Guid workspaceGuid)
96107
{
97-
if (workspaceRootFolderCache.TryGetValue(workspaceGuid, out var folderGuid))
108+
if (workspaceRootFolderCache.TryGetValue(workspaceGuid, out var info))
98109
{
99-
return folderGuid;
110+
return info;
100111
}
101112

102113
var workspace = workspaceService.GetWorkspace(workspaceGuid) ?? throw new Exception($"Required workspace(GUID={workspaceGuid}) not found");
103114

104-
folderGuid = ContentFolderInfo.Provider.Get()
115+
info = ContentFolderInfo.Provider.Get()
105116
.WhereEquals(nameof(ContentFolderInfo.ContentFolderWorkspaceID), workspace.WorkspaceID)
106117
.And().WhereEquals(nameof(ContentFolderInfo.ContentFolderParentFolderID), null)
107-
.FirstOrDefault()?.ContentFolderGUID
108-
?? throw new Exception($"Root folder for workspace(GUID={workspaceGuid}) not found");
118+
.FirstOrDefault()
119+
?? throw new Exception($"Root folder for workspace(GUID={workspaceGuid}) not found");
109120

110-
workspaceRootFolderCache[workspaceGuid] = folderGuid;
121+
workspaceRootFolderCache[workspaceGuid] = info;
111122

112-
return folderGuid;
123+
return info;
113124
}
114125

115126
private static string DisplayNamePathToTreePath(string displayNamePath) => string.Join("/", displayNamePath.Split('/').Select(x => ValidationHelper.GetCodeName(x, 0)));
@@ -118,8 +129,8 @@ public Guid GetWorkspaceRootFolder(Guid workspaceGuid)
118129
/// <summary>
119130
/// Returns standard attributes of a new folder derived from its display name
120131
/// </summary>
121-
public static (Guid Guid, string Name, string DisplayName, string PathSegmentName) StandardFolderTemplate(string siteHash, string folderDisplayName, string absoluteDisplayNamePath)
122-
=> (GuidHelper.CreateFolderGuid($"{siteHash}|{DisplayNamePathToTreePath(absoluteDisplayNamePath)}"), FolderDisplayNameToName(folderDisplayName), folderDisplayName, FolderDisplayNameToName(folderDisplayName));
132+
public static (Guid Guid, string Name, string DisplayName, string PathSegmentName) StandardFolderTemplate(string siteHash, string folderDisplayName, string absoluteDisplayNamePath, Guid workspaceGuid)
133+
=> (GuidHelper.CreateFolderGuid($"{workspaceGuid}|{siteHash}|{DisplayNamePathToTreePath(absoluteDisplayNamePath)}"), FolderDisplayNameToName(folderDisplayName), folderDisplayName, FolderDisplayNameToName(folderDisplayName));
123134

124135
public delegate void FolderPathSegmentCallback(string segmentDisplayName, string path);
125136

@@ -141,10 +152,10 @@ public static void WalkFolderPath(string path, FolderPathSegmentCallback segment
141152
/// Accepts path where segments represent folder display names and returns path template,
142153
/// i.e. sequence of folder templates, composed of standard folder templates derived from the display names
143154
/// </summary>
144-
public static List<(Guid Guid, string Name, string DisplayName, string PathSegmentName)> StandardPathTemplate(string siteHash, string absolutePath)
155+
public static List<(Guid Guid, string Name, string DisplayName, string PathSegmentName)> StandardPathTemplate(string siteHash, string absolutePath, Guid workspaceGuid)
145156
{
146157
var pathTemplate = new List<(Guid Guid, string Name, string DisplayName, string PathSegmentName)>();
147-
WalkFolderPath(absolutePath, (segmentDisplayName, path) => pathTemplate.Add(StandardFolderTemplate(siteHash, segmentDisplayName, path)));
158+
WalkFolderPath(absolutePath, (segmentDisplayName, path) => pathTemplate.Add(StandardFolderTemplate(siteHash, segmentDisplayName, path, workspaceGuid)));
148159
return pathTemplate;
149160
}
150161

@@ -153,13 +164,13 @@ public static void WalkFolderPath(string path, FolderPathSegmentCallback segment
153164
/// of the folders and the attributes of a folder that is to be created are derived from its display name
154165
/// in a defined standard way
155166
/// </summary>
156-
public Task<Guid?> EnsureStandardFolderStructure(string siteHash, string absolutePath, Guid? workspaceGuid = null) => EnsureFolderStructure(StandardPathTemplate(siteHash, absolutePath), workspaceGuid);
167+
public Task<Guid?> EnsureStandardFolderStructure(string siteHash, string absolutePath, Guid? workspaceGuid = null) => EnsureFolderStructure(StandardPathTemplate(siteHash, absolutePath, workspaceGuid!.Value), workspaceGuid);
157168

158169
public Guid? EnsureFolder(ContentFolderOptions? options, bool isReusableItem, Guid? workspaceGuid = null) =>
159170
isReusableItem
160171
? options switch
161172
{
162-
null => GetWorkspaceRootFolder(workspaceService.FallbackWorkspace.Value.WorkspaceGUID),
173+
null => GetWorkspaceRootFolder(workspaceService.FallbackWorkspace.Value.WorkspaceGUID).ContentFolderGUID,
163174
{ Guid: { } guid } => guid,
164175
{ DisplayNamePath: { } displayNamePath } => EnsureStandardFolderStructure("customtables", displayNamePath, workspaceGuid).GetAwaiter().GetResult(),
165176
_ => throw new InvalidOperationException($"{nameof(ContentFolderOptions)} has neither {nameof(ContentFolderOptions.Guid)} nor {nameof(ContentFolderOptions.DisplayNamePath)} specified")

0 commit comments

Comments
 (0)