Files
Umbraco-CMS/src/Umbraco.Core/Packaging/PackageInstallation.cs
2019-01-14 17:46:12 +11:00

186 lines
9.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Packaging;
using Umbraco.Core.Services;
namespace Umbraco.Core.Packaging
{
internal class PackageInstallation : IPackageInstallation
{
private readonly PackageExtraction _packageExtraction;
private readonly PackageDataInstallation _packageDataInstallation;
private readonly PackageFileInstallation _packageFileInstallation;
private readonly CompiledPackageXmlParser _parser;
private readonly IPackageActionRunner _packageActionRunner;
private readonly string _packagesFolderPath;
private readonly DirectoryInfo _packageExtractionFolder;
private readonly DirectoryInfo _applicationRootFolder;
/// <summary>
/// Constructor
/// </summary>
/// <param name="packageDataInstallation"></param>
/// <param name="packageFileInstallation"></param>
/// <param name="parser"></param>
/// <param name="packageActionRunner"></param>
/// <param name="packagesFolderPath">
/// The relative path of the package storage folder (i.e. ~/App_Data/Packages )
/// </param>
/// <param name="applicationRootFolder">
/// The root folder of the application
/// </param>
/// <param name="packageExtractionFolder">
/// The destination root folder to extract the package files (generally the same as applicationRoot) but can be modified for testing
/// </param>
public PackageInstallation(PackageDataInstallation packageDataInstallation, PackageFileInstallation packageFileInstallation, CompiledPackageXmlParser parser, IPackageActionRunner packageActionRunner,
string packagesFolderPath, DirectoryInfo applicationRootFolder, DirectoryInfo packageExtractionFolder)
{
_packageExtraction = new PackageExtraction();
_packageFileInstallation = packageFileInstallation ?? throw new ArgumentNullException(nameof(packageFileInstallation));
_packageDataInstallation = packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation));
_parser = parser;
_packageActionRunner = packageActionRunner;
_packagesFolderPath = packagesFolderPath;
_applicationRootFolder = applicationRootFolder;
_packageExtractionFolder = packageExtractionFolder;
}
public CompiledPackage ReadPackage(string packageFileName)
{
if (packageFileName == null) throw new ArgumentNullException(nameof(packageFileName));
var packageZipFile = GetPackageZipFile(packageFileName);
var doc = GetConfigXmlDoc(packageZipFile);
var compiledPackage = _parser.ToCompiledPackage(doc, Path.GetFileName(packageZipFile.FullName), _applicationRootFolder.FullName);
ValidatePackageFile(packageZipFile, compiledPackage);
return compiledPackage;
}
public IEnumerable<string> InstallPackageFiles(PackageDefinition packageDefinition, CompiledPackage compiledPackage, int userId)
{
if (packageDefinition == null) throw new ArgumentNullException(nameof(packageDefinition));
if (compiledPackage == null) throw new ArgumentNullException(nameof(compiledPackage));
//these should be the same, TODO: we should have a better validator for this
if (packageDefinition.Name != compiledPackage.Name)
throw new InvalidOperationException("The package definition does not match the compiled package manifest");
var packageZipFile = GetPackageZipFile(compiledPackage.PackageFileName);
return _packageFileInstallation.InstallFiles(compiledPackage, packageZipFile, _packageExtractionFolder.FullName);
}
public InstallationSummary InstallPackageData(PackageDefinition packageDefinition, CompiledPackage compiledPackage, int userId)
{
//TODO: Update the PackageDefinition!
var installationSummary = new InstallationSummary
{
DataTypesInstalled = _packageDataInstallation.ImportDataTypes(compiledPackage.DataTypes.ToList(), userId),
LanguagesInstalled = _packageDataInstallation.ImportLanguages(compiledPackage.Languages, userId),
DictionaryItemsInstalled = _packageDataInstallation.ImportDictionaryItems(compiledPackage.DictionaryItems, userId),
MacrosInstalled = _packageDataInstallation.ImportMacros(compiledPackage.Macros, userId),
TemplatesInstalled = _packageDataInstallation.ImportTemplates(compiledPackage.Templates.ToList(), userId),
DocumentTypesInstalled = _packageDataInstallation.ImportDocumentTypes(compiledPackage.DocumentTypes, userId)
};
//we need a reference to the imported doc types to continue
var importedDocTypes = installationSummary.DocumentTypesInstalled.ToDictionary(x => x.Alias, x => x);
installationSummary.StylesheetsInstalled = _packageDataInstallation.ImportStylesheets(compiledPackage.Stylesheets, userId);
installationSummary.ContentInstalled = _packageDataInstallation.ImportContent(compiledPackage.Documents, importedDocTypes, userId);
installationSummary.Actions = _parser.GetPackageActions(XElement.Parse(compiledPackage.Actions), compiledPackage.Name);
installationSummary.MetaData = compiledPackage;
//fixme: Verify that this will work!
installationSummary.FilesInstalled = packageDefinition.Files;
installationSummary.PackageInstalled = true;
//make sure the definition is up to date with everything
foreach (var x in installationSummary.DataTypesInstalled) packageDefinition.DataTypes.Add(x.Id.ToInvariantString());
foreach (var x in installationSummary.LanguagesInstalled) packageDefinition.Languages.Add(x.Id.ToInvariantString());
foreach (var x in installationSummary.DictionaryItemsInstalled) packageDefinition.DictionaryItems.Add(x.Id.ToInvariantString());
foreach (var x in installationSummary.MacrosInstalled) packageDefinition.Macros.Add(x.Id.ToInvariantString());
foreach (var x in installationSummary.TemplatesInstalled) packageDefinition.Templates.Add(x.Id.ToInvariantString());
foreach (var x in installationSummary.DocumentTypesInstalled) packageDefinition.DocumentTypes.Add(x.Id.ToInvariantString());
foreach (var x in installationSummary.StylesheetsInstalled) packageDefinition.Stylesheets.Add(x.Id.ToInvariantString());
var contentInstalled = installationSummary.ContentInstalled.ToList();
packageDefinition.ContentNodeId = contentInstalled.Count > 0 ? contentInstalled[0].Id.ToInvariantString() : null;
RunPackageActions(packageDefinition, installationSummary.Actions);
return installationSummary;
}
private void RunPackageActions(PackageDefinition packageDefinition, IEnumerable<PackageAction> actions)
{
foreach (var n in actions)
{
var undo = n.Undo;
if (undo)
packageDefinition.Actions += n.XmlData.ToString();
//Run the actions tagged only for 'install'
if (n.RunAt != ActionRunAt.Install) continue;
if (n.Alias.IsNullOrWhiteSpace() == false)
_packageActionRunner.RunPackageAction(packageDefinition.Name, n.Alias, n.XmlData);
}
}
private FileInfo GetPackageZipFile(string packageFileName) => new FileInfo(IOHelper.MapPath(_packagesFolderPath).EnsureEndsWith('\\') + packageFileName);
private XDocument GetConfigXmlDoc(FileInfo packageFile)
{
var configXmlContent = _packageExtraction.ReadTextFileFromArchive(packageFile, "package.xml", out _);
var document = XDocument.Parse(configXmlContent);
if (document.Root == null ||
document.Root.Name.LocalName.Equals("umbPackage") == false)
throw new FormatException("xml does not have a root node called \"umbPackage\"");
return document;
}
private void ValidatePackageFile(FileInfo packageFile, CompiledPackage package)
{
if (!(package.Files?.Count > 0)) return;
var sourceDestination = _parser.ExtractSourceDestinationFileInformation(package.Files).ToArray();
var missingFiles = _packageExtraction.FindMissingFiles(packageFile, sourceDestination.Select(i => i.packageUniqueFile)).ToArray();
if (missingFiles.Any())
{
throw new Exception("The following file(s) are missing in the package: " +
string.Join(", ", missingFiles.Select(
mf =>
{
var (packageUniqueFile, appRelativePath) = sourceDestination.Single(fi => fi.packageUniqueFile == mf);
return $"source: \"{packageUniqueFile}\" destination: \"{appRelativePath}\"";
})));
}
IEnumerable<string> duplicates = _packageExtraction.FindDuplicateFileNames(packageFile).ToArray();
if (duplicates.Any())
{
throw new Exception("The following filename(s) are found more than one time in the package, since the filename is used ad primary key, this is not allowed: " +
string.Join(", ", duplicates));
}
}
}
}