diff --git a/src/Umbraco.Core/Packaging/PackageMigrationResource.cs b/src/Umbraco.Core/Packaging/PackageMigrationResource.cs index 2f407df88f..a4f3465705 100644 --- a/src/Umbraco.Core/Packaging/PackageMigrationResource.cs +++ b/src/Umbraco.Core/Packaging/PackageMigrationResource.cs @@ -12,17 +12,52 @@ namespace Umbraco.Cms.Core.Packaging { public static class PackageMigrationResource { - private static Stream GetEmbeddedPackageStream(Type planType) + private static Stream GetEmbeddedPackageZipStream(Type planType) { // lookup the embedded resource by convention Assembly currentAssembly = planType.Assembly; var fileName = $"{planType.Namespace}.package.zip"; Stream stream = currentAssembly.GetManifestResourceStream(fileName); + + return stream; + } + + public static XDocument GetEmbeddedPackageDataManifest(Type planType, out ZipArchive zipArchive) + { + XDocument packageXml; + var zipStream = GetEmbeddedPackageZipStream(planType); + if (zipStream is not null) + { + zipArchive = GetPackageDataManifest(zipStream, out packageXml); + return packageXml; + } + + zipArchive = null; + packageXml = GetEmbeddedPackageXmlDoc(planType); + return packageXml; + } + + public static XDocument GetEmbeddedPackageDataManifest(Type planType) + { + return GetEmbeddedPackageDataManifest(planType, out _); + } + + private static XDocument GetEmbeddedPackageXmlDoc(Type planType) + { + // lookup the embedded resource by convention + Assembly currentAssembly = planType.Assembly; + var fileName = $"{planType.Namespace}.package.xml"; + Stream stream = currentAssembly.GetManifestResourceStream(fileName); if (stream == null) { - throw new FileNotFoundException("Cannot find the embedded file.", fileName); + return null; } - return stream; + XDocument xml; + using (stream) + { + xml = XDocument.Load(stream); + } + return xml; } public static string GetEmbeddedPackageDataManifestHash(Type planType) @@ -30,17 +65,46 @@ namespace Umbraco.Cms.Core.Packaging // SEE: HashFromStreams in the benchmarks project for how fast this is. It will run // on every startup for every embedded package.zip. The bigger the zip, the more time it takes. // But it is still very fast ~303ms for a 100MB file. This will only be an issue if there are - // several very large package.zips. + // several very large package.zips. - using Stream stream = GetEmbeddedPackageStream(planType); - return stream.GetStreamHash(); + using Stream stream = GetEmbeddedPackageZipStream(planType); + + if (stream is not null) + { + return stream.GetStreamHash(); + } + + var xml = GetEmbeddedPackageXmlDoc(planType); + + if (xml is not null) + { + return stream.GetStreamHash(); + } + + throw new IOException("Missing embedded files for planType: " + planType); } - public static ZipArchive GetEmbeddedPackageDataManifest(Type planType, out XDocument packageXml) - => GetPackageDataManifest(GetEmbeddedPackageStream(planType), out packageXml); + public static bool TryGetEmbeddedPackageDataManifest(Type planType, out XDocument packageXml, out ZipArchive zipArchive) + { + var zipStream = GetEmbeddedPackageZipStream(planType); + if (zipStream is not null) + { + zipArchive = GetPackageDataManifest(zipStream, out packageXml); + return true; + } + + zipArchive = null; + packageXml = GetEmbeddedPackageXmlDoc(planType); + return packageXml is not null; + } public static ZipArchive GetPackageDataManifest(Stream packageZipStream, out XDocument packageXml) { + if (packageZipStream == null) + { + throw new ArgumentNullException(nameof(packageZipStream)); + } + var zip = new ZipArchive(packageZipStream, ZipArchiveMode.Read); ZipArchiveEntry packageXmlEntry = zip.GetEntry("package.xml"); if (packageXmlEntry == null) diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index ffc67663cc..a24890d5f2 100644 --- a/src/Umbraco.Core/Packaging/PackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs @@ -209,29 +209,46 @@ namespace Umbraco.Cms.Core.Packaging PackageDataTypes(definition, root); Dictionary mediaFiles = PackageMedia(definition, root); - var tempPackagePath = temporaryPath + "/package.zip"; - - using (FileStream fileStream = File.OpenWrite(tempPackagePath)) - using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) + string fileName; + string tempPackagePath; + if (mediaFiles.Count > 0) { - ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); - using (Stream entryStream = packageXmlEntry.Open()) + fileName = "package.zip"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) { - compiledPackageXml.Save(entryStream); - } - - foreach (KeyValuePair mediaFile in mediaFiles) - { - var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; - ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); - using (Stream entryStream = mediaEntry.Open()) - using (mediaFile.Value) + ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); + using (Stream entryStream = packageXmlEntry.Open()) { - mediaFile.Value.Seek(0, SeekOrigin.Begin); - mediaFile.Value.CopyTo(entryStream); + compiledPackageXml.Save(entryStream); + } + + foreach (KeyValuePair mediaFile in mediaFiles) + { + var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; + ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); + using (Stream entryStream = mediaEntry.Open()) + using (mediaFile.Value) + { + mediaFile.Value.Seek(0, SeekOrigin.Begin); + mediaFile.Value.CopyTo(entryStream); + } } } } + else + { + fileName = "package.xml"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + { + compiledPackageXml.Save(fileStream); + } + } + + var directoryName = _hostingEnvironment.MapPathWebRoot(Path.Combine(_mediaFolderPath, definition.Name.Replace(' ', '_'))); @@ -241,7 +258,7 @@ namespace Umbraco.Cms.Core.Packaging Directory.CreateDirectory(directoryName); } - var finalPackagePath = Path.Combine(directoryName, "package.zip"); + var finalPackagePath = Path.Combine(directoryName, fileName); if (File.Exists(finalPackagePath)) { @@ -347,7 +364,7 @@ namespace Umbraco.Cms.Core.Packaging } else if (items.ContainsKey(dictionaryItem.ParentId.Value)) { - // we know the parent exists in the dictionary but + // we know the parent exists in the dictionary but // we haven't processed it yet so we'll leave it for the next loop continue; } diff --git a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs index b16326ea56..b2d3754329 100644 --- a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs +++ b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs @@ -19,12 +19,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging { internal class ImportPackageBuilderExpression : MigrationExpressionBase { - private readonly IPackagingService _packagingService; - private readonly IMediaService _mediaService; - private readonly MediaFileManager _mediaFileManager; - private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly IShortStringHelper _shortStringHelper; private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + private readonly MediaFileManager _mediaFileManager; + private readonly IMediaService _mediaService; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly IPackagingService _packagingService; + private readonly IShortStringHelper _shortStringHelper; private bool _executed; public ImportPackageBuilderExpression( @@ -45,7 +45,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging } /// - /// The type of the migration which dictates the namespace of the embedded resource + /// The type of the migration which dictates the namespace of the embedded resource /// public Type EmbeddedResourceMigrationType { get; set; } @@ -63,68 +63,78 @@ namespace Umbraco.Cms.Infrastructure.Packaging if (EmbeddedResourceMigrationType == null && PackageDataManifest == null) { - throw new InvalidOperationException($"Nothing to execute, neither {nameof(EmbeddedResourceMigrationType)} or {nameof(PackageDataManifest)} has been set."); + throw new InvalidOperationException( + $"Nothing to execute, neither {nameof(EmbeddedResourceMigrationType)} or {nameof(PackageDataManifest)} has been set."); } InstallationSummary installationSummary; if (EmbeddedResourceMigrationType != null) { - // get the embedded resource - using (ZipArchive zipPackage = PackageMigrationResource.GetEmbeddedPackageDataManifest( + if (PackageMigrationResource.TryGetEmbeddedPackageDataManifest( EmbeddedResourceMigrationType, - out XDocument xml)) + out XDocument xml, out ZipArchive zipPackage)) { // first install the package installationSummary = _packagingService.InstallCompiledPackageData(xml); - // then we need to save each file to the saved media items - var mediaWithFiles = xml.XPathSelectElements( - "./umbPackage/MediaItems/MediaSet//*[@id][@mediaFilePath]") - .ToDictionary( - x => x.AttributeValue("key"), - x => x.AttributeValue("mediaFilePath")); - - // Any existing media by GUID will not be installed by the package service, it will just be skipped - // so you cannot 'update' media (or content) using a package since those are not schema type items. - // This means you cannot 'update' the media file either. The installationSummary.MediaInstalled - // will be empty for any existing media which means that the files will also not be updated. - foreach (IMedia media in installationSummary.MediaInstalled) + if (zipPackage is not null) { - if (mediaWithFiles.TryGetValue(media.Key, out var mediaFilePath)) + // get the embedded resource + using (zipPackage) { - // this is a media item that has a file, so find that file in the zip - var entryPath = $"media{mediaFilePath.EnsureStartsWith('/')}"; - ZipArchiveEntry mediaEntry = zipPackage.GetEntry(entryPath); - if (mediaEntry == null) - { - throw new InvalidOperationException("No media file found in package zip for path " + entryPath); - } + // then we need to save each file to the saved media items + var mediaWithFiles = xml.XPathSelectElements( + "./umbPackage/MediaItems/MediaSet//*[@id][@mediaFilePath]") + .ToDictionary( + x => x.AttributeValue("key"), + x => x.AttributeValue("mediaFilePath")); - // read the media file and save it to the media item - // using the current file system provider. - using (Stream mediaStream = mediaEntry.Open()) + // Any existing media by GUID will not be installed by the package service, it will just be skipped + // so you cannot 'update' media (or content) using a package since those are not schema type items. + // This means you cannot 'update' the media file either. The installationSummary.MediaInstalled + // will be empty for any existing media which means that the files will also not be updated. + foreach (IMedia media in installationSummary.MediaInstalled) { - media.SetValue( - _mediaFileManager, - _mediaUrlGenerators, - _shortStringHelper, - _contentTypeBaseServiceProvider, - Constants.Conventions.Media.File, - Path.GetFileName(mediaFilePath), - mediaStream); - } + if (mediaWithFiles.TryGetValue(media.Key, out var mediaFilePath)) + { + // this is a media item that has a file, so find that file in the zip + var entryPath = $"media{mediaFilePath.EnsureStartsWith('/')}"; + ZipArchiveEntry mediaEntry = zipPackage.GetEntry(entryPath); + if (mediaEntry == null) + { + throw new InvalidOperationException( + "No media file found in package zip for path " + + entryPath); + } - _mediaService.Save(media); + // read the media file and save it to the media item + // using the current file system provider. + using (Stream mediaStream = mediaEntry.Open()) + { + media.SetValue( + _mediaFileManager, + _mediaUrlGenerators, + _shortStringHelper, + _contentTypeBaseServiceProvider, + Constants.Conventions.Media.File, + Path.GetFileName(mediaFilePath), + mediaStream); + } + + _mediaService.Save(media); + } + } } } } - } - else - { - installationSummary = _packagingService.InstallCompiledPackageData(PackageDataManifest); - } + else + { + installationSummary = _packagingService.InstallCompiledPackageData(PackageDataManifest); + } - Logger.LogInformation($"Package migration executed. Summary: {installationSummary}"); + Logger.LogInformation($"Package migration executed. Summary: {installationSummary}"); + } + throw new InvalidOperationException($"The package zip or xml found."); } } } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Core/Packaging/CreatedPackagesRepositoryTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Core/Packaging/CreatedPackagesRepositoryTests.cs index 3ee9b1375c..ca584cf2a7 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Core/Packaging/CreatedPackagesRepositoryTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Core/Packaging/CreatedPackagesRepositoryTests.cs @@ -36,7 +36,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Packaging [TearDown] public void DeleteTestFolder() => Directory.Delete(HostingEnvironment.MapPathContentRoot("~/" + _testBaseFolder), true); - + private IContentService ContentService => GetRequiredService(); private IContentTypeService ContentTypeService => GetRequiredService(); @@ -164,7 +164,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Packaging { var parent = new DictionaryItem("Parent") { - Key = Guid.NewGuid() + Key = Guid.NewGuid() }; LocalizationService.Save(parent); var child1 = new DictionaryItem(parent.Key, "Child1") @@ -204,7 +204,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Packaging }; PackageBuilder.SavePackage(def); - + string packageXmlPath = PackageBuilder.ExportPackage(def); using (var packageZipStream = File.OpenRead(packageXmlPath))