v9: Move local xml package files to database instead (#11654)
* Move package to database * Added migration and implemented new repository * Updated migrations to use proper xml convert * Fixed save function and renamed createDate to update date * Updated dependencyInjection * Updated UmbracoPlan.cs * Apply suggestions from code review Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> * Added File check * Tried using same context as create table * Fix DTO * Fix GetById and local package saving * Fix images when migrating * Implement deletion of all local files * Only delete local repo file, not file snapshots * Remove static package path and use the one we save * Update package repo to export package and remove check for ids when exporting * Minor fixes * Fix so that you can download package after creating * Update savePackage method to export package afterwards Co-authored-by: Nikolaj Geisle <niko737@edu.ucl.dk> Co-authored-by: Elitsa Marinovska <elm@umbraco.dk> Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com>
This commit is contained in:
@@ -367,7 +367,7 @@ namespace Umbraco.Extensions
|
||||
/// <param name="content"><see cref="IContent"/> to generate xml for</param>
|
||||
/// <param name="serializer"></param>
|
||||
/// <returns>Xml representation of the passed in <see cref="IContent"/></returns>
|
||||
internal static XElement ToDeepXml(this IContent content, IEntityXmlSerializer serializer)
|
||||
public static XElement ToDeepXml(this IContent content, IEntityXmlSerializer serializer)
|
||||
{
|
||||
return serializer.Serialize(content, false, true);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Umbraco.Cms.Core.Packaging
|
||||
/// <summary>
|
||||
/// Manages the storage of installed/created package definitions
|
||||
/// </summary>
|
||||
[Obsolete("Packages have now been moved to the database instead of local files, please use CreatedPackageSchemaRepository instead")]
|
||||
public class PackagesRepository : ICreatedPackagesRepository
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
@@ -744,5 +745,13 @@ namespace Umbraco.Cms.Core.Packaging
|
||||
var packagesXml = XDocument.Load(packagesFile);
|
||||
return packagesXml;
|
||||
}
|
||||
|
||||
public void DeleteLocalRepositoryFiles()
|
||||
{
|
||||
var packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile);
|
||||
File.Delete(packagesFile);
|
||||
var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath);
|
||||
Directory.Delete(packagesFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,8 @@ namespace Umbraco.Cms.Core
|
||||
public const string UserLogin = TableNamePrefix + "UserLogin";
|
||||
|
||||
public const string LogViewerQuery = TableNamePrefix + "LogViewerQuery";
|
||||
|
||||
public const string CreatedPackageSchema = TableNamePrefix + "CreatedPackageSchema";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Implement;
|
||||
using Umbraco.Cms.Infrastructure.Packaging;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
|
||||
using Umbraco.Cms.Infrastructure.Services.Implement;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -74,16 +76,14 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
|
||||
|
||||
builder.Services.AddUnique<ConflictingPackageData>();
|
||||
builder.Services.AddUnique<CompiledPackageXmlParser>();
|
||||
builder.Services.AddUnique<ICreatedPackagesRepository>(factory => CreatePackageRepository(factory, "createdPackages.config"));
|
||||
builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config"));
|
||||
builder.Services.AddUnique<ICreatedPackagesRepository, CreatedPackageSchemaRepository>();
|
||||
builder.Services.AddUnique<PackageDataInstallation>();
|
||||
builder.Services.AddUnique<IPackageInstallation, PackageInstallation>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of PackagesRepository for either the ICreatedPackagesRepository or the IInstalledPackagesRepository
|
||||
/// </summary>
|
||||
private static PackagesRepository CreatePackageRepository(IServiceProvider factory, string packageRepoFileName)
|
||||
=> new PackagesRepository(
|
||||
factory.GetRequiredService<IContentService>(),
|
||||
|
||||
@@ -78,7 +78,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
|
||||
typeof(ContentScheduleDto),
|
||||
typeof(LogViewerQueryDto),
|
||||
typeof(ContentVersionCleanupPolicyDto),
|
||||
typeof(UserGroup2NodeDto)
|
||||
typeof(UserGroup2NodeDto),
|
||||
typeof(CreatedPackageSchemaDto)
|
||||
};
|
||||
|
||||
private readonly IUmbracoDatabase _database;
|
||||
|
||||
@@ -268,6 +268,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
|
||||
// TO 9.2.0
|
||||
To<AddUserGroup2NodeTable>("{0571C395-8F0B-44E9-8E3F-47BDD08D817B}");
|
||||
To<AddDefaultForNotificationsToggle>("{AD3D3B7F-8E74-45A4-85DB-7FFAD57F9243}");
|
||||
To<MovePackageXMLToDb>("{A2F22F17-5870-4179-8A8D-2362AA4A0A5F}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Umbraco.Cms.Core.Packaging;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0
|
||||
{
|
||||
public class MovePackageXMLToDb : MigrationBase
|
||||
{
|
||||
private readonly PackagesRepository _packagesRepository;
|
||||
private readonly PackageDefinitionXmlParser _xmlParser;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MovePackageXMLToDb"/> class.
|
||||
/// </summary>
|
||||
public MovePackageXMLToDb(IMigrationContext context, PackagesRepository packagesRepository)
|
||||
: base(context)
|
||||
{
|
||||
_packagesRepository = packagesRepository;
|
||||
_xmlParser = new PackageDefinitionXmlParser();
|
||||
}
|
||||
|
||||
private void CreateDatabaseTable()
|
||||
{
|
||||
// Add CreatedPackage table in database if it doesn't exist
|
||||
IEnumerable<string> tables = SqlSyntax.GetTablesInSchema(Context.Database);
|
||||
if (!tables.InvariantContains(CreatedPackageSchemaDto.TableName))
|
||||
{
|
||||
Create.Table<CreatedPackageSchemaDto>().Do();
|
||||
}
|
||||
}
|
||||
|
||||
private void MigrateCreatedPackageFilesToDb()
|
||||
{
|
||||
// Load data from file
|
||||
IEnumerable<PackageDefinition> packages = _packagesRepository.GetAll();
|
||||
var createdPackageDtos = new List<CreatedPackageSchemaDto>();
|
||||
foreach (PackageDefinition package in packages)
|
||||
{
|
||||
// Create dto from xmlDocument
|
||||
var dto = new CreatedPackageSchemaDto()
|
||||
{
|
||||
Name = package.Name,
|
||||
Value = _xmlParser.ToXml(package).ToString(),
|
||||
UpdateDate = DateTime.Now,
|
||||
PackageId = Guid.NewGuid()
|
||||
};
|
||||
createdPackageDtos.Add(dto);
|
||||
}
|
||||
|
||||
_packagesRepository.DeleteLocalRepositoryFiles();
|
||||
if (createdPackageDtos.Any())
|
||||
{
|
||||
// Insert dto into CreatedPackage table
|
||||
Database.InsertBulk(createdPackageDtos);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Migrate()
|
||||
{
|
||||
CreateDatabaseTable();
|
||||
MigrateCreatedPackageFilesToDb();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
|
||||
{
|
||||
[TableName(TableName)]
|
||||
[ExplicitColumns]
|
||||
[PrimaryKey("id")]
|
||||
public class CreatedPackageSchemaDto
|
||||
{
|
||||
public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.CreatedPackageSchema;
|
||||
|
||||
[Column("id")]
|
||||
[PrimaryKeyColumn]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Column("name")]
|
||||
[Length(255)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
[Index(IndexTypes.UniqueNonClustered, ForColumns = "name", Name = "IX_" + TableName + "_Name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Column("value")]
|
||||
[SpecialDbType(SpecialDbTypes.NVARCHARMAX)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
public string Value { get; set; }
|
||||
|
||||
[Column("updateDate")]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
[Column("packageId")]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
public Guid PackageId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,758 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Packaging;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Extensions;
|
||||
using File = System.IO.File;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class CreatedPackageSchemaRepository : ICreatedPackagesRepository
|
||||
{
|
||||
private readonly PackageDefinitionXmlParser _xmlParser;
|
||||
private readonly IUmbracoDatabase _umbracoDatabase;
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
private readonly FileSystems _fileSystems;
|
||||
private readonly IEntityXmlSerializer _serializer;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly IMediaService _mediaService;
|
||||
private readonly IMediaTypeService _mediaTypeService;
|
||||
private readonly IContentService _contentService;
|
||||
private readonly MediaFileManager _mediaFileManager;
|
||||
private readonly IMacroService _macroService;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly string _tempFolderPath;
|
||||
private readonly string _mediaFolderPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CreatedPackageSchemaRepository"/> class.
|
||||
/// </summary>
|
||||
public CreatedPackageSchemaRepository(
|
||||
IUmbracoDatabase umbracoDatabase,
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
FileSystems fileSystems,
|
||||
IEntityXmlSerializer serializer,
|
||||
IDataTypeService dataTypeService,
|
||||
ILocalizationService localizationService,
|
||||
IFileService fileService,
|
||||
IMediaService mediaService,
|
||||
IMediaTypeService mediaTypeService,
|
||||
IContentService contentService,
|
||||
MediaFileManager mediaFileManager,
|
||||
IMacroService macroService,
|
||||
IContentTypeService contentTypeService,
|
||||
string mediaFolderPath = null,
|
||||
string tempFolderPath = null)
|
||||
{
|
||||
_umbracoDatabase = umbracoDatabase;
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_fileSystems = fileSystems;
|
||||
_serializer = serializer;
|
||||
_dataTypeService = dataTypeService;
|
||||
_localizationService = localizationService;
|
||||
_fileService = fileService;
|
||||
_mediaService = mediaService;
|
||||
_mediaTypeService = mediaTypeService;
|
||||
_contentService = contentService;
|
||||
_mediaFileManager = mediaFileManager;
|
||||
_macroService = macroService;
|
||||
_contentTypeService = contentTypeService;
|
||||
_xmlParser = new PackageDefinitionXmlParser();
|
||||
_mediaFolderPath = mediaFolderPath ?? globalSettings.Value.UmbracoMediaPath + "/created-packages";
|
||||
_tempFolderPath =
|
||||
tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles";
|
||||
}
|
||||
|
||||
public IEnumerable<PackageDefinition> GetAll()
|
||||
{
|
||||
Sql<ISqlContext> query = new Sql<ISqlContext>(_umbracoDatabase.SqlContext)
|
||||
.Select<CreatedPackageSchemaDto>()
|
||||
.From<CreatedPackageSchemaDto>()
|
||||
.OrderBy<CreatedPackageSchemaDto>(x => x.Id);
|
||||
|
||||
var packageDefinitions = new List<PackageDefinition>();
|
||||
|
||||
List<CreatedPackageSchemaDto> xmlSchemas = _umbracoDatabase.Fetch<CreatedPackageSchemaDto>(query);
|
||||
foreach (CreatedPackageSchemaDto packageSchema in xmlSchemas)
|
||||
{
|
||||
var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value));
|
||||
packageDefinition.Id = packageSchema.Id;
|
||||
packageDefinition.Name = packageSchema.Name;
|
||||
packageDefinition.PackageId = packageSchema.PackageId;
|
||||
packageDefinitions.Add(packageDefinition);
|
||||
}
|
||||
|
||||
return packageDefinitions;
|
||||
}
|
||||
|
||||
public PackageDefinition GetById(int id)
|
||||
{
|
||||
Sql<ISqlContext> query = new Sql<ISqlContext>(_umbracoDatabase.SqlContext)
|
||||
.Select<CreatedPackageSchemaDto>()
|
||||
.From<CreatedPackageSchemaDto>()
|
||||
.Where<CreatedPackageSchemaDto>(x => x.Id == id);
|
||||
List<CreatedPackageSchemaDto> schemaDtos = _umbracoDatabase.Fetch<CreatedPackageSchemaDto>(query);
|
||||
|
||||
if (schemaDtos.IsCollectionEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var packageSchema = schemaDtos.First();
|
||||
var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value));
|
||||
packageDefinition.Id = packageSchema.Id;
|
||||
packageDefinition.Name = packageSchema.Name;
|
||||
packageDefinition.PackageId = packageSchema.PackageId;
|
||||
return packageDefinition;
|
||||
}
|
||||
|
||||
public void Delete(int id)
|
||||
{
|
||||
// Delete package snapshot
|
||||
var packageDef = GetById(id);
|
||||
if (File.Exists(packageDef.PackagePath))
|
||||
{
|
||||
File.Delete(packageDef.PackagePath);
|
||||
}
|
||||
|
||||
Sql<ISqlContext> query = new Sql<ISqlContext>(_umbracoDatabase.SqlContext)
|
||||
.Delete<CreatedPackageSchemaDto>()
|
||||
.Where<CreatedPackageSchemaDto>(x => x.Id == id);
|
||||
|
||||
_umbracoDatabase.Delete<CreatedPackageSchemaDto>(query);
|
||||
}
|
||||
|
||||
public bool SavePackage(PackageDefinition definition)
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
throw new NullReferenceException("PackageDefinition cannot be null when saving");
|
||||
}
|
||||
|
||||
if (definition.Name == null || string.IsNullOrEmpty(definition.Name) || definition.PackagePath == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure it's valid
|
||||
ValidatePackage(definition);
|
||||
|
||||
|
||||
if (definition.Id == default)
|
||||
{
|
||||
// Create dto from definition
|
||||
var dto = new CreatedPackageSchemaDto()
|
||||
{
|
||||
Name = definition.Name,
|
||||
Value = _xmlParser.ToXml(definition).ToString(),
|
||||
UpdateDate = DateTime.Now,
|
||||
PackageId = Guid.NewGuid()
|
||||
};
|
||||
|
||||
// Set the ids, we have to save in database first to get the Id
|
||||
definition.PackageId = dto.PackageId;
|
||||
var result = _umbracoDatabase.Insert(dto);
|
||||
var decimalResult = result.SafeCast<decimal>();
|
||||
definition.Id = decimal.ToInt32(decimalResult);
|
||||
}
|
||||
|
||||
// Save snapshot locally, we do this to the updated packagePath
|
||||
ExportPackage(definition);
|
||||
// Create dto from definition
|
||||
var updatedDto = new CreatedPackageSchemaDto()
|
||||
{
|
||||
Name = definition.Name,
|
||||
Value = _xmlParser.ToXml(definition).ToString(),
|
||||
Id = definition.Id,
|
||||
PackageId = definition.PackageId,
|
||||
UpdateDate = DateTime.Now
|
||||
};
|
||||
_umbracoDatabase.Update(updatedDto);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public string ExportPackage(PackageDefinition definition)
|
||||
{
|
||||
|
||||
// Ensure it's valid
|
||||
ValidatePackage(definition);
|
||||
|
||||
// Create a folder for building this package
|
||||
var temporaryPath =
|
||||
_hostingEnvironment.MapPathContentRoot(_tempFolderPath.EnsureEndsWith('/') + Guid.NewGuid());
|
||||
if (Directory.Exists(temporaryPath) == false)
|
||||
{
|
||||
Directory.CreateDirectory(temporaryPath);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Init package file
|
||||
XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root);
|
||||
|
||||
// Info section
|
||||
root.Add(GetPackageInfoXml(definition));
|
||||
|
||||
PackageDocumentsAndTags(definition, root);
|
||||
PackageDocumentTypes(definition, root);
|
||||
PackageMediaTypes(definition, root);
|
||||
PackageTemplates(definition, root);
|
||||
PackageStylesheets(definition, root);
|
||||
PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem);
|
||||
PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View",
|
||||
_fileSystems.PartialViewsFileSystem);
|
||||
PackageMacros(definition, root);
|
||||
PackageDictionaryItems(definition, root);
|
||||
PackageLanguages(definition, root);
|
||||
PackageDataTypes(definition, root);
|
||||
Dictionary<string, Stream> mediaFiles = PackageMedia(definition, root);
|
||||
|
||||
string fileName;
|
||||
string tempPackagePath;
|
||||
if (mediaFiles.Count > 0)
|
||||
{
|
||||
fileName = "package.zip";
|
||||
tempPackagePath = Path.Combine(temporaryPath, fileName);
|
||||
using (FileStream fileStream = File.OpenWrite(tempPackagePath))
|
||||
using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true))
|
||||
{
|
||||
ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml");
|
||||
using (Stream entryStream = packageXmlEntry.Open())
|
||||
{
|
||||
compiledPackageXml.Save(entryStream);
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, Stream> 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(' ', '_')));
|
||||
|
||||
if (Directory.Exists(directoryName) == false)
|
||||
{
|
||||
Directory.CreateDirectory(directoryName);
|
||||
}
|
||||
|
||||
var finalPackagePath = Path.Combine(directoryName, fileName);
|
||||
|
||||
if (File.Exists(finalPackagePath))
|
||||
{
|
||||
File.Delete(finalPackagePath);
|
||||
}
|
||||
|
||||
if (File.Exists(finalPackagePath.Replace("zip", "xml")))
|
||||
{
|
||||
File.Delete(finalPackagePath.Replace("zip", "xml"));
|
||||
}
|
||||
|
||||
File.Move(tempPackagePath, finalPackagePath);
|
||||
|
||||
definition.PackagePath = finalPackagePath;
|
||||
|
||||
return finalPackagePath;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clean up
|
||||
Directory.Delete(temporaryPath, true);
|
||||
}
|
||||
}
|
||||
|
||||
private XDocument CreateCompiledPackageXml(out XElement root)
|
||||
{
|
||||
root = new XElement("umbPackage");
|
||||
var compiledPackageXml = new XDocument(root);
|
||||
return compiledPackageXml;
|
||||
}
|
||||
|
||||
private void ValidatePackage(PackageDefinition definition)
|
||||
{
|
||||
// Ensure it's valid
|
||||
var context = new ValidationContext(definition, serviceProvider: null, items: null);
|
||||
var results = new List<ValidationResult>();
|
||||
var isValid = Validator.TryValidateObject(definition, context, results);
|
||||
if (!isValid)
|
||||
{
|
||||
throw new InvalidOperationException("Validation failed, there is invalid data on the model: " +
|
||||
string.Join(", ", results.Select(x => x.ErrorMessage)));
|
||||
}
|
||||
}
|
||||
|
||||
private void PackageDataTypes(PackageDefinition definition, XContainer root)
|
||||
{
|
||||
var dataTypes = new XElement("DataTypes");
|
||||
foreach (var dtId in definition.DataTypes)
|
||||
{
|
||||
if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IDataType dataType = _dataTypeService.GetDataType(outInt);
|
||||
if (dataType == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dataTypes.Add(_serializer.Serialize(dataType));
|
||||
}
|
||||
|
||||
root.Add(dataTypes);
|
||||
}
|
||||
|
||||
private void PackageLanguages(PackageDefinition definition, XContainer root)
|
||||
{
|
||||
var languages = new XElement("Languages");
|
||||
foreach (var langId in definition.Languages)
|
||||
{
|
||||
if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ILanguage lang = _localizationService.GetLanguageById(outInt);
|
||||
if (lang == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
languages.Add(_serializer.Serialize(lang));
|
||||
}
|
||||
|
||||
root.Add(languages);
|
||||
}
|
||||
|
||||
private void PackageDictionaryItems(PackageDefinition definition, XContainer root)
|
||||
{
|
||||
var rootDictionaryItems = new XElement("DictionaryItems");
|
||||
var items = new Dictionary<Guid, (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue)>();
|
||||
|
||||
foreach (var dictionaryId in definition.DictionaryItems)
|
||||
{
|
||||
if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IDictionaryItem di = _localizationService.GetDictionaryItemById(outInt);
|
||||
|
||||
if (di == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
items[di.Key] = (di, _serializer.Serialize(di, false));
|
||||
}
|
||||
|
||||
// organize them in hierarchy ...
|
||||
var itemCount = items.Count;
|
||||
var processed = new Dictionary<Guid, XElement>();
|
||||
while (processed.Count < itemCount)
|
||||
{
|
||||
foreach (Guid key in items.Keys.ToList())
|
||||
{
|
||||
(IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key];
|
||||
|
||||
if (!dictionaryItem.ParentId.HasValue)
|
||||
{
|
||||
// if it has no parent, its definitely just at the root
|
||||
AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (processed.ContainsKey(dictionaryItem.ParentId.Value))
|
||||
{
|
||||
// we've processed this parent element already so we can just append this xml child to it
|
||||
AppendDictionaryElement(processed[dictionaryItem.ParentId.Value], items, processed, key,
|
||||
serializedDictionaryValue);
|
||||
}
|
||||
else if (items.ContainsKey(dictionaryItem.ParentId.Value))
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
else
|
||||
{
|
||||
// in this case, the parent of this item doesn't exist in our collection, we have no
|
||||
// choice but to add it to the root.
|
||||
AppendDictionaryElement(rootDictionaryItems, items, processed, key,
|
||||
serializedDictionaryValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.Add(rootDictionaryItems);
|
||||
|
||||
static void AppendDictionaryElement(XElement rootDictionaryItems,
|
||||
Dictionary<Guid, (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue)> items,
|
||||
Dictionary<Guid, XElement> processed, Guid key, XElement serializedDictionaryValue)
|
||||
{
|
||||
// track it
|
||||
processed.Add(key, serializedDictionaryValue);
|
||||
|
||||
// append it
|
||||
rootDictionaryItems.Add(serializedDictionaryValue);
|
||||
|
||||
// remove it so its not re-processed
|
||||
items.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void PackageMacros(PackageDefinition definition, XContainer root)
|
||||
{
|
||||
var packagedMacros = new List<IMacro>();
|
||||
var macros = new XElement("Macros");
|
||||
foreach (var macroId in definition.Macros)
|
||||
{
|
||||
if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int outInt))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
XElement macroXml = GetMacroXml(outInt, out IMacro macro);
|
||||
if (macroXml == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
macros.Add(macroXml);
|
||||
packagedMacros.Add(macro);
|
||||
}
|
||||
|
||||
root.Add(macros);
|
||||
|
||||
// Get the partial views for macros and package those (exclude views outside of the default directory, e.g. App_Plugins\*\Views)
|
||||
IEnumerable<string> views = packagedMacros
|
||||
.Where(x => x.MacroSource.StartsWith(Constants.SystemDirectories.MacroPartials))
|
||||
.Select(x =>
|
||||
x.MacroSource.Substring(Constants.SystemDirectories.MacroPartials.Length).Replace('/', '\\'));
|
||||
PackageStaticFiles(views, root, "MacroPartialViews", "View", _fileSystems.MacroPartialsFileSystem);
|
||||
}
|
||||
|
||||
private void PackageStylesheets(PackageDefinition definition, XContainer root)
|
||||
{
|
||||
var stylesheetsXml = new XElement("Stylesheets");
|
||||
foreach (var stylesheet in definition.Stylesheets)
|
||||
{
|
||||
if (stylesheet.IsNullOrWhiteSpace())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
XElement xml = GetStylesheetXml(stylesheet, true);
|
||||
if (xml != null)
|
||||
{
|
||||
stylesheetsXml.Add(xml);
|
||||
}
|
||||
}
|
||||
|
||||
root.Add(stylesheetsXml);
|
||||
}
|
||||
|
||||
private void PackageStaticFiles(
|
||||
IEnumerable<string> filePaths,
|
||||
XContainer root,
|
||||
string containerName,
|
||||
string elementName,
|
||||
IFileSystem fileSystem)
|
||||
{
|
||||
var scriptsXml = new XElement(containerName);
|
||||
foreach (var file in filePaths)
|
||||
{
|
||||
if (file.IsNullOrWhiteSpace())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!fileSystem.FileExists(file))
|
||||
{
|
||||
throw new InvalidOperationException("No file found with path " + file);
|
||||
}
|
||||
|
||||
using (Stream stream = fileSystem.OpenFile(file))
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var fileContents = reader.ReadToEnd();
|
||||
scriptsXml.Add(
|
||||
new XElement(
|
||||
elementName,
|
||||
new XAttribute("path", file),
|
||||
new XCData(fileContents)));
|
||||
}
|
||||
}
|
||||
|
||||
root.Add(scriptsXml);
|
||||
}
|
||||
|
||||
private void PackageTemplates(PackageDefinition definition, XContainer root)
|
||||
{
|
||||
var templatesXml = new XElement("Templates");
|
||||
foreach (var templateId in definition.Templates)
|
||||
{
|
||||
if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ITemplate template = _fileService.GetTemplate(outInt);
|
||||
if (template == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
templatesXml.Add(_serializer.Serialize(template));
|
||||
}
|
||||
|
||||
root.Add(templatesXml);
|
||||
}
|
||||
|
||||
private void PackageDocumentTypes(PackageDefinition definition, XContainer root)
|
||||
{
|
||||
var contentTypes = new HashSet<IContentType>();
|
||||
var docTypesXml = new XElement("DocumentTypes");
|
||||
foreach (var dtId in definition.DocumentTypes)
|
||||
{
|
||||
if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IContentType contentType = _contentTypeService.Get(outInt);
|
||||
if (contentType == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddDocumentType(contentType, contentTypes);
|
||||
}
|
||||
|
||||
foreach (IContentType contentType in contentTypes)
|
||||
{
|
||||
docTypesXml.Add(_serializer.Serialize(contentType));
|
||||
}
|
||||
|
||||
root.Add(docTypesXml);
|
||||
}
|
||||
|
||||
private void PackageMediaTypes(PackageDefinition definition, XContainer root)
|
||||
{
|
||||
var mediaTypes = new HashSet<IMediaType>();
|
||||
var mediaTypesXml = new XElement("MediaTypes");
|
||||
foreach (var mediaTypeId in definition.MediaTypes)
|
||||
{
|
||||
if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IMediaType mediaType = _mediaTypeService.Get(outInt);
|
||||
if (mediaType == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddMediaType(mediaType, mediaTypes);
|
||||
}
|
||||
|
||||
foreach (IMediaType mediaType in mediaTypes)
|
||||
{
|
||||
mediaTypesXml.Add(_serializer.Serialize(mediaType));
|
||||
}
|
||||
|
||||
root.Add(mediaTypesXml);
|
||||
}
|
||||
|
||||
private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root)
|
||||
{
|
||||
// Documents and tags
|
||||
if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse(definition.ContentNodeId,
|
||||
NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentNodeId))
|
||||
{
|
||||
if (contentNodeId > 0)
|
||||
{
|
||||
// load content from umbraco.
|
||||
IContent content = _contentService.GetById(contentNodeId);
|
||||
if (content != null)
|
||||
{
|
||||
var contentXml = definition.ContentLoadChildNodes
|
||||
? content.ToDeepXml(_serializer)
|
||||
: content.ToXml(_serializer);
|
||||
|
||||
// Create the Documents/DocumentSet node
|
||||
|
||||
root.Add(
|
||||
new XElement(
|
||||
"Documents",
|
||||
new XElement(
|
||||
"DocumentSet",
|
||||
new XAttribute("importMode", "root"),
|
||||
contentXml)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, Stream> PackageMedia(PackageDefinition definition, XElement root)
|
||||
{
|
||||
var mediaStreams = new Dictionary<string, Stream>();
|
||||
|
||||
// callback that occurs on each serialized media item
|
||||
void OnSerializedMedia(IMedia media, XElement xmlMedia)
|
||||
{
|
||||
// get the media file path and store that separately in the XML.
|
||||
// the media file path is different from the URL and is specifically
|
||||
// extracted using the property editor for this media file and the current media file system.
|
||||
Stream mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath);
|
||||
if (mediaStream != null)
|
||||
{
|
||||
xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath));
|
||||
|
||||
// add the stream to our outgoing stream
|
||||
mediaStreams.Add(mediaFilePath, mediaStream);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<IMedia> medias = _mediaService.GetByIds(definition.MediaUdis);
|
||||
|
||||
var mediaXml = new XElement(
|
||||
"MediaItems",
|
||||
medias.Select(media =>
|
||||
{
|
||||
XElement serializedMedia = _serializer.Serialize(
|
||||
media,
|
||||
definition.MediaLoadChildNodes,
|
||||
OnSerializedMedia);
|
||||
|
||||
return new XElement("MediaSet", serializedMedia);
|
||||
}));
|
||||
|
||||
root.Add(mediaXml);
|
||||
|
||||
return mediaStreams;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a macros xml node
|
||||
/// </summary>
|
||||
private XElement GetMacroXml(int macroId, out IMacro macro)
|
||||
{
|
||||
macro = _macroService.GetById(macroId);
|
||||
if (macro == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
XElement xml = _serializer.Serialize(macro);
|
||||
return xml;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a umbraco stylesheet to a package xml node
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the stylesheet.</param>
|
||||
/// <param name="includeProperties">if set to <c>true</c> [include properties].</param>
|
||||
private XElement GetStylesheetXml(string path, bool includeProperties)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentException("Value cannot be null or whitespace.", nameof(path));
|
||||
}
|
||||
|
||||
IStylesheet stylesheet = _fileService.GetStylesheet(path);
|
||||
if (stylesheet == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _serializer.Serialize(stylesheet, includeProperties);
|
||||
}
|
||||
|
||||
private void AddDocumentType(IContentType dt, HashSet<IContentType> dtl)
|
||||
{
|
||||
if (dt.ParentId > 0)
|
||||
{
|
||||
IContentType parent = _contentTypeService.Get(dt.ParentId);
|
||||
if (parent != null)
|
||||
{
|
||||
AddDocumentType(parent, dtl);
|
||||
}
|
||||
}
|
||||
|
||||
if (!dtl.Contains(dt))
|
||||
{
|
||||
dtl.Add(dt);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMediaType(IMediaType mediaType, HashSet<IMediaType> mediaTypes)
|
||||
{
|
||||
if (mediaType.ParentId > 0)
|
||||
{
|
||||
IMediaType parent = _mediaTypeService.Get(mediaType.ParentId);
|
||||
if (parent != null)
|
||||
{
|
||||
AddMediaType(parent, mediaTypes);
|
||||
}
|
||||
}
|
||||
|
||||
if (!mediaTypes.Contains(mediaType))
|
||||
{
|
||||
mediaTypes.Add(mediaType);
|
||||
}
|
||||
}
|
||||
|
||||
private static XElement GetPackageInfoXml(PackageDefinition definition)
|
||||
{
|
||||
var info = new XElement("info");
|
||||
|
||||
// Package info
|
||||
var package = new XElement("package");
|
||||
package.Add(new XElement("name", definition.Name));
|
||||
info.Add(package);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
if (ModelState.IsValid == false)
|
||||
return ValidationProblem(ModelState);
|
||||
|
||||
//save it
|
||||
// Save it
|
||||
if (!_packagingService.SaveCreatedPackage(model))
|
||||
{
|
||||
return ValidationProblem(
|
||||
@@ -78,9 +78,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
: $"The package with id {model.Id} was not found");
|
||||
}
|
||||
|
||||
_packagingService.ExportCreatedPackage(model);
|
||||
|
||||
//the packagePath will be on the model
|
||||
// The packagePath will be on the model
|
||||
return model;
|
||||
}
|
||||
|
||||
@@ -112,11 +110,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
|
||||
return ValidationErrorResult.CreateNotificationValidationErrorResult(
|
||||
$"Package migration failed on package {packageName} with error: {ex.Message}. Check log for full details.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult DownloadCreatedPackage(int id)
|
||||
public IActionResult DownloadCreatedPackage(int id)
|
||||
{
|
||||
var package = _packagingService.GetCreatedPackageById(id);
|
||||
if (package == null)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Packaging;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Packaging
|
||||
{
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
|
||||
public class CreatedPackageSchemaTests : UmbracoIntegrationTest
|
||||
{
|
||||
private ICreatedPackagesRepository CreatedPackageSchemaRepository =>
|
||||
GetRequiredService<ICreatedPackagesRepository>();
|
||||
|
||||
[Test]
|
||||
public void PackagesRepository_Can_Save_PackageDefinition()
|
||||
{
|
||||
var packageDefinition = new PackageDefinition()
|
||||
{
|
||||
Name = "NewPack", DocumentTypes = new List<string>() { "Root" }
|
||||
};
|
||||
var result = CreatedPackageSchemaRepository.SavePackage(packageDefinition);
|
||||
Assert.IsTrue(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PackageRepository_GetAll_Returns_All_PackageDefinitions()
|
||||
{
|
||||
var packageDefinitionList = new List<PackageDefinition>()
|
||||
{
|
||||
new () { Name = "PackOne" },
|
||||
new () { Name = "PackTwo" },
|
||||
new () { Name = "PackThree" }
|
||||
};
|
||||
foreach (PackageDefinition packageDefinition in packageDefinitionList)
|
||||
{
|
||||
CreatedPackageSchemaRepository.SavePackage(packageDefinition);
|
||||
}
|
||||
|
||||
var loadedPackageDefinitions = CreatedPackageSchemaRepository.GetAll().ToList();
|
||||
CollectionAssert.IsNotEmpty(loadedPackageDefinitions);
|
||||
CollectionAssert.AllItemsAreUnique(loadedPackageDefinitions);
|
||||
Assert.AreEqual(loadedPackageDefinitions.Count, 3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PackageRepository_Can_Update_Package()
|
||||
{
|
||||
var packageDefinition = new PackageDefinition() { Name = "TestPackage" };
|
||||
CreatedPackageSchemaRepository.SavePackage(packageDefinition);
|
||||
|
||||
packageDefinition.Name = "UpdatedName";
|
||||
CreatedPackageSchemaRepository.SavePackage(packageDefinition);
|
||||
var result = CreatedPackageSchemaRepository.GetAll().ToList();
|
||||
|
||||
Assert.AreEqual(result.Count, 1);
|
||||
Assert.AreEqual(result.FirstOrDefault()?.Name, "UpdatedName");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user