From 80794f3efd80e1470e731effed7f8d72dad1a192 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Fri, 17 May 2024 14:35:18 +0200 Subject: [PATCH] [V14] import/export media/document type endpoints (#16100) * Working import/export media/document types * WIP * Refactoring of import doctype/media types - added analyze endpoint to extract relevant data without fully processing the file - split up import endpoints into POST & PUT - removed availableAtAction as the new endpoint allows clients to call the POST/PUT endpoints with confidence - Added a new service that is responsible for turning temp files into Import compatible XML and being able to extracty partial information from it * Wrap persistance access in scopes * Typos, formatting, clean-up * PR feedback * update openapi spec * Changed deleteFile flag to _temporaryFileService.EnlistDeleteIfScopeCompletes * Itty bitty typo * Moved magic cleanup into its own method so orchestration can decide when. --------- Co-authored-by: Sven Geusens Co-authored-by: kjac --- .../DocumentTypeControllerBase.cs | 26 + .../ExportDocumentTypeController.cs | 41 + .../ImportExistingDocumentTypeController.cs | 44 + .../ImportNewDocumentTypeController.cs | 46 + .../Import/AnalyzeImportController.cs | 39 + .../Import/ImportControllerBase.cs | 23 + .../MediaType/ExportMediaTypeController.cs | 41 + .../ImportExistingMediaTypeController.cs | 44 + .../MediaType/ImportNewMediaTypeController.cs | 44 + .../MediaType/MediaTypeControllerBase.cs | 27 + .../ExportBuilderExtensions.cs | 15 + .../ImportBuilderExtensions.cs | 16 + .../UmbracoBuilderExtensions.cs | 4 +- .../Factories/IUdtFileContentFactory.cs | 11 + .../Factories/UdtFileContentFactory.cs | 35 + ...tyImportAnalysisViewModelsMapDefinition.cs | 20 + src/Umbraco.Cms.Api.Management/OpenApi.json | 821 ++++++++++++++++++ .../Services/DictionaryItemImportService.cs | 4 +- .../ImportDocumentTypeRequestModel.cs | 6 + .../EntityImportAnalysisResponseModel.cs | 10 + .../MediaType/ImportMediaTypeRequestModel.cs | 6 + .../DependencyInjection/UmbracoBuilder.cs | 6 + .../Models/ImportExport/EntityXmlAnalysis.cs | 12 + .../Services/EntityXmlSerializer.cs | 8 +- .../Services/IEntityXmlSerializer.cs | 3 + .../Services/IPackageDataInstallation.cs | 80 ++ .../ImportExport/ContentTypeImportService.cs | 93 ++ .../ImportExport/IContentTypeImportService.cs | 9 + .../ImportExport/IMediaTypeImportService.cs | 12 + .../ITemporaryFileToXmlImportService.cs | 19 + .../ImportExport/MediaTypeImportService.cs | 88 ++ .../TemporaryFileToXmlImportService.cs | 100 +++ .../ContentTypeImportOperationStatus.cs | 12 + .../MediaTypeImportOperationStatus.cs | 12 + .../TemporaryFileXmlImportOperationStatus.cs | 8 + .../UmbracoBuilder.Services.cs | 4 +- .../Packaging/PackageDataInstallation.cs | 16 +- .../Packaging/PackageInstallation.cs | 5 +- .../Packaging/PackageDataInstallationTests.cs | 2 +- 39 files changed, 1795 insertions(+), 17 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ExportDocumentTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ImportExistingDocumentTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ImportNewDocumentTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Import/AnalyzeImportController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Import/ImportControllerBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/MediaType/ExportMediaTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/MediaType/ImportExistingMediaTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/MediaType/ImportNewMediaTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/DependencyInjection/ExportBuilderExtensions.cs create mode 100644 src/Umbraco.Cms.Api.Management/DependencyInjection/ImportBuilderExtensions.cs create mode 100644 src/Umbraco.Cms.Api.Management/Factories/IUdtFileContentFactory.cs create mode 100644 src/Umbraco.Cms.Api.Management/Factories/UdtFileContentFactory.cs create mode 100644 src/Umbraco.Cms.Api.Management/Mapping/Import/EntityImportAnalysisViewModelsMapDefinition.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/ImportDocumentTypeRequestModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Import/EntityImportAnalysisResponseModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/MediaType/ImportMediaTypeRequestModel.cs create mode 100644 src/Umbraco.Core/Models/ImportExport/EntityXmlAnalysis.cs create mode 100644 src/Umbraco.Core/Services/IPackageDataInstallation.cs create mode 100644 src/Umbraco.Core/Services/ImportExport/ContentTypeImportService.cs create mode 100644 src/Umbraco.Core/Services/ImportExport/IContentTypeImportService.cs create mode 100644 src/Umbraco.Core/Services/ImportExport/IMediaTypeImportService.cs create mode 100644 src/Umbraco.Core/Services/ImportExport/ITemporaryFileToXmlImportService.cs create mode 100644 src/Umbraco.Core/Services/ImportExport/MediaTypeImportService.cs create mode 100644 src/Umbraco.Core/Services/ImportExport/TemporaryFileToXmlImportService.cs create mode 100644 src/Umbraco.Core/Services/OperationStatus/ContentTypeImportOperationStatus.cs create mode 100644 src/Umbraco.Core/Services/OperationStatus/MediaTypeImportOperationStatus.cs create mode 100644 src/Umbraco.Core/Services/OperationStatus/TemporaryFileXmlImportOperationStatus.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs index 763a1e2934..76ed318cfc 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.ViewModels.DocumentType; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Web.Common.Authorization; @@ -108,6 +109,31 @@ public abstract class DocumentTypeControllerBase : ManagementApiControllerBase _ => new ObjectResult("Unknown content type structure operation status") { StatusCode = StatusCodes.Status500InternalServerError } }); + protected IActionResult ContentTypeImportOperationStatusResult(ContentTypeImportOperationStatus operationStatus) => + OperationStatusResult(operationStatus, problemDetailsBuilder => operationStatus switch + { + ContentTypeImportOperationStatus.TemporaryFileNotFound => NotFound(problemDetailsBuilder + .WithTitle("Temporary file not found") + .Build()), + ContentTypeImportOperationStatus.TemporaryFileConversionFailure => BadRequest(problemDetailsBuilder + .WithTitle("Failed to convert the specified file") + .WithDetail("The import failed due to not being able to convert the file into proper xml") + .Build()), + ContentTypeImportOperationStatus.DocumentTypeExists => BadRequest(problemDetailsBuilder + .WithTitle("Failed to import because document type exits") + .WithDetail("The import failed because the document type that was being imported already exits") + .Build()), + ContentTypeImportOperationStatus.TypeMismatch => BadRequest(problemDetailsBuilder + .WithTitle("Type Mismatch") + .WithDetail("The import failed because the file contained an entity that is not a content type.") + .Build()), + ContentTypeImportOperationStatus.IdMismatch => BadRequest(problemDetailsBuilder + .WithTitle("Invalid Id") + .WithDetail("The import failed because the id of the document type you are trying to update did not match the id in the file.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown document type import operation status.") + }); + protected IActionResult ContentEditingOperationStatusResult(ContentEditingOperationStatus status) => OperationStatusResult(status, problemDetailsBuilder => status switch { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ExportDocumentTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ExportDocumentTypeController.cs new file mode 100644 index 0000000000..d0c66d0599 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ExportDocumentTypeController.cs @@ -0,0 +1,41 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; + +[ApiVersion("1.0")] +public class ExportDocumentTypeController : DocumentTypeControllerBase +{ + private readonly IContentTypeService _contentTypeService; + private readonly IUdtFileContentFactory _fileContentFactory; + + public ExportDocumentTypeController( + IContentTypeService contentTypeService, + IUdtFileContentFactory fileContentFactory) + { + _contentTypeService = contentTypeService; + _fileContentFactory = fileContentFactory; + } + + [HttpGet("{id:guid}/export")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public IActionResult Export( + CancellationToken cancellationToken, + Guid id) + { + IContentType? contentType = _contentTypeService.Get(id); + if (contentType is null) + { + return OperationStatusResult(ContentTypeOperationStatus.NotFound); + } + + return _fileContentFactory.Create(contentType); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ImportExistingDocumentTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ImportExistingDocumentTypeController.cs new file mode 100644 index 0000000000..9b1d6506a1 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ImportExistingDocumentTypeController.cs @@ -0,0 +1,44 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services.ImportExport; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; + +[ApiVersion("1.0")] +public class ImportExistingDocumentTypeController : DocumentTypeControllerBase +{ + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IContentTypeImportService _contentTypeImportService; + + public ImportExistingDocumentTypeController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IContentTypeImportService contentTypeImportService) + { + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _contentTypeImportService = contentTypeImportService; + } + + [HttpPut("{id:guid}/import")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task Import( + CancellationToken cancellationToken, + Guid id, + ImportDocumentTypeRequestModel model) + { + Attempt importAttempt = await _contentTypeImportService.Import(model.File.Id, CurrentUserKey(_backOfficeSecurityAccessor), id); + + return importAttempt.Success is false + ? ContentTypeImportOperationStatusResult(importAttempt.Status) + : Ok(); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ImportNewDocumentTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ImportNewDocumentTypeController.cs new file mode 100644 index 0000000000..5b4fbde199 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ImportNewDocumentTypeController.cs @@ -0,0 +1,46 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services.ImportExport; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; + +[ApiVersion("1.0")] +public class ImportNewDocumentTypeController : DocumentTypeControllerBase +{ + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IContentTypeImportService _contentTypeImportService; + + public ImportNewDocumentTypeController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IContentTypeImportService contentTypeImportService) + { + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _contentTypeImportService = contentTypeImportService; + } + + [HttpPost("import")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task Import( + CancellationToken cancellationToken, + ImportDocumentTypeRequestModel model) + { + + Attempt importAttempt = await _contentTypeImportService.Import(model.File.Id, CurrentUserKey(_backOfficeSecurityAccessor)); + + return importAttempt.Success is false + ? ContentTypeImportOperationStatusResult(importAttempt.Status) + : CreatedAtId( + controller => nameof(controller.ByKey), + importAttempt.Result!.Key); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Import/AnalyzeImportController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Import/AnalyzeImportController.cs new file mode 100644 index 0000000000..5f9a98df98 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Import/AnalyzeImportController.cs @@ -0,0 +1,39 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Import; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models.ImportExport; +using Umbraco.Cms.Core.Services.ImportExport; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Import; + +[ApiVersion("1.0")] +public class AnalyzeImportController : ImportControllerBase +{ + private readonly ITemporaryFileToXmlImportService _temporaryFileToXmlImportService; + private readonly IUmbracoMapper _mapper; + + public AnalyzeImportController( + ITemporaryFileToXmlImportService temporaryFileToXmlImportService, + IUmbracoMapper mapper) + { + _temporaryFileToXmlImportService = temporaryFileToXmlImportService; + _mapper = mapper; + } + + [HttpGet("analyze")] + [ProducesResponseType(typeof(EntityImportAnalysisResponseModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task Analyze(CancellationToken cancellationToken, Guid temporaryFileId) + { + Attempt analyzeResult = await _temporaryFileToXmlImportService.AnalyzeAsync(temporaryFileId); + + return analyzeResult.Success is false + ? TemporaryFileXmlImportOperationStatusResult(analyzeResult.Status) + : Ok(_mapper.Map(analyzeResult.Result)); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Import/ImportControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Import/ImportControllerBase.cs new file mode 100644 index 0000000000..fa89fdf0e8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Import/ImportControllerBase.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Import; + +[VersionedApiBackOfficeRoute("import")] +[ApiExplorerSettings(GroupName = "Import")] +public abstract class ImportControllerBase : ManagementApiControllerBase +{ + protected static IActionResult TemporaryFileXmlImportOperationStatusResult(TemporaryFileXmlImportOperationStatus operationStatus) => + OperationStatusResult(operationStatus, problemDetailsBuilder => operationStatus switch + { + TemporaryFileXmlImportOperationStatus.TemporaryFileNotFound => new NotFoundObjectResult(problemDetailsBuilder + .WithTitle("Temporary file not found") + .Build()), + TemporaryFileXmlImportOperationStatus.UndeterminedEntityType => new BadRequestObjectResult(problemDetailsBuilder + .WithTitle("Unable to determine entity type") + .Build()), + _ => new ObjectResult("Unknown temporary file import operation status") { StatusCode = StatusCodes.Status500InternalServerError }, + }); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ExportMediaTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ExportMediaTypeController.cs new file mode 100644 index 0000000000..12f8540a4e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ExportMediaTypeController.cs @@ -0,0 +1,41 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.MediaType; + +[ApiVersion("1.0")] +public class ExportMediaTypeController : MediaTypeControllerBase +{ + private readonly IMediaTypeService _mediaTypeService; + private readonly IUdtFileContentFactory _fileContentFactory; + + public ExportMediaTypeController( + IMediaTypeService mediaTypeService, + IUdtFileContentFactory fileContentFactory) + { + _mediaTypeService = mediaTypeService; + _fileContentFactory = fileContentFactory; + } + + [HttpGet("{id:guid}/export")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public IActionResult Export( + CancellationToken cancellationToken, + Guid id) + { + IMediaType? mediaType = _mediaTypeService.Get(id); + if (mediaType is null) + { + return OperationStatusResult(ContentTypeOperationStatus.NotFound); + } + + return _fileContentFactory.Create(mediaType); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ImportExistingMediaTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ImportExistingMediaTypeController.cs new file mode 100644 index 0000000000..9c4ba5ed95 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ImportExistingMediaTypeController.cs @@ -0,0 +1,44 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.MediaType; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services.ImportExport; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.MediaType; + +[ApiVersion("1.0")] +public class ImportExistingMediaTypeController : MediaTypeControllerBase +{ + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IMediaTypeImportService _mediaTypeImportService; + + public ImportExistingMediaTypeController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IMediaTypeImportService mediaTypeImportService) + { + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _mediaTypeImportService = mediaTypeImportService; + } + + [HttpPut("{id:guid}/import")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task Import( + CancellationToken cancellationToken, + Guid id, + ImportMediaTypeRequestModel model) + { + Attempt importAttempt = await _mediaTypeImportService.Import(model.File.Id, CurrentUserKey(_backOfficeSecurityAccessor)); + + return importAttempt.Success is false + ? MediaTypeImportOperationStatusResult(importAttempt.Status) + : Ok(); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ImportNewMediaTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ImportNewMediaTypeController.cs new file mode 100644 index 0000000000..868822e494 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ImportNewMediaTypeController.cs @@ -0,0 +1,44 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Controllers.DocumentType; +using Umbraco.Cms.Api.Management.ViewModels.MediaType; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services.ImportExport; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.MediaType; + +[ApiVersion("1.0")] +public class ImportNewMediaTypeController : MediaTypeControllerBase +{ + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IMediaTypeImportService _mediaTypeImportService; + + public ImportNewMediaTypeController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IMediaTypeImportService mediaTypeImportService) + { + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _mediaTypeImportService = mediaTypeImportService; + } + + [HttpPost("import")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task Import( + CancellationToken cancellationToken, + ImportMediaTypeRequestModel model) + { + Attempt importAttempt = await _mediaTypeImportService.Import(model.File.Id, CurrentUserKey(_backOfficeSecurityAccessor)); + + return importAttempt.Success is false + ? MediaTypeImportOperationStatusResult(importAttempt.Status) + : CreatedAtId(controller => nameof(controller.ByKey), importAttempt.Result!.Key); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/MediaTypeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/MediaTypeControllerBase.cs index dd3cec55ad..df68fc7ec2 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/MediaTypeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/MediaTypeControllerBase.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Controllers.DocumentType; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.ViewModels.MediaType; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Web.Common.Authorization; @@ -18,4 +20,29 @@ public abstract class MediaTypeControllerBase : ManagementApiControllerBase protected IActionResult StructureOperationStatusResult(ContentTypeStructureOperationStatus status) => DocumentTypeControllerBase.ContentTypeStructureOperationStatusResult(status, "media"); + + protected IActionResult MediaTypeImportOperationStatusResult(MediaTypeImportOperationStatus operationStatus) => + OperationStatusResult(operationStatus, problemDetailsBuilder => operationStatus switch + { + MediaTypeImportOperationStatus.TemporaryFileNotFound => NotFound(problemDetailsBuilder + .WithTitle("Temporary file not found") + .Build()), + MediaTypeImportOperationStatus.TemporaryFileConversionFailure => BadRequest(problemDetailsBuilder + .WithTitle("Failed to convert the specified file") + .WithDetail("The import failed due to not being able to convert the file into proper xml.") + .Build()), + MediaTypeImportOperationStatus.MediaTypeExists => BadRequest(problemDetailsBuilder + .WithTitle("Failed to import because media type exits") + .WithDetail("The import failed because the media type that was being imported already exits.") + .Build()), + MediaTypeImportOperationStatus.TypeMismatch => BadRequest(problemDetailsBuilder + .WithTitle("Type Mismatch") + .WithDetail("The import failed because the file contained an entity that is not a media type.") + .Build()), + MediaTypeImportOperationStatus.IdMismatch => BadRequest(problemDetailsBuilder + .WithTitle("Invalid Id") + .WithDetail("The import failed because the id of the media type you are trying to update did not match the id in the file.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown media type import operation status.") + }); } diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/ExportBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/ExportBuilderExtensions.cs new file mode 100644 index 0000000000..f66bbad3ec --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/ExportBuilderExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class ExportBuilderExtensions +{ + internal static IUmbracoBuilder AddExport(this IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/ImportBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/ImportBuilderExtensions.cs new file mode 100644 index 0000000000..0815dd7f5b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/ImportBuilderExtensions.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Api.Management.Mapping.Import; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class ImportBuilderExtensions +{ + internal static IUmbracoBuilder AddImport(this IUmbracoBuilder builder) + { + builder.WithCollectionBuilder() + .Add(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs index 58a3434e6c..a670dcab47 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs @@ -68,7 +68,9 @@ public static partial class UmbracoBuilderExtensions .AddPasswordConfiguration() .AddSupplemenataryLocalizedTextFileSources() .AddUserData() - .AddSegment(); + .AddSegment() + .AddExport() + .AddImport(); services .ConfigureOptions() diff --git a/src/Umbraco.Cms.Api.Management/Factories/IUdtFileContentFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IUdtFileContentFactory.cs new file mode 100644 index 0000000000..fa8aacf438 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IUdtFileContentFactory.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.Factories; + +public interface IUdtFileContentFactory +{ + FileContentResult Create(IContentType contentType); + + FileContentResult Create(IMediaType mediaType); +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/UdtFileContentFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UdtFileContentFactory.cs new file mode 100644 index 0000000000..5b55e6b13a --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/UdtFileContentFactory.cs @@ -0,0 +1,35 @@ +using System.Net.Mime; +using System.Text; +using System.Xml.Linq; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Factories; + +public class UdtFileContentFactory : IUdtFileContentFactory +{ + private readonly IEntityXmlSerializer _entityXmlSerializer; + + public UdtFileContentFactory(IEntityXmlSerializer entityXmlSerializer) + => _entityXmlSerializer = entityXmlSerializer; + + public FileContentResult Create(IContentType contentType) + { + XElement xml = _entityXmlSerializer.Serialize(contentType); + return XmlTofile(contentType, xml); + } + + public FileContentResult Create(IMediaType mediaType) + { + XElement xml = _entityXmlSerializer.Serialize(mediaType); + return XmlTofile(mediaType, xml); + } + + private static FileContentResult XmlTofile(IContentTypeBase contentTypeBase, XElement xml) => + new(Encoding.UTF8.GetBytes(xml.ToDataString()), MediaTypeNames.Application.Octet) + { + FileDownloadName = $"{contentTypeBase.Alias}.udt" + }; +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Import/EntityImportAnalysisViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Import/EntityImportAnalysisViewModelsMapDefinition.cs new file mode 100644 index 0000000000..918c694e8c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/Import/EntityImportAnalysisViewModelsMapDefinition.cs @@ -0,0 +1,20 @@ +using Umbraco.Cms.Api.Management.ViewModels.Import; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models.ImportExport; + +namespace Umbraco.Cms.Api.Management.Mapping.Import; + +public class EntityImportAnalysisViewModelsMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((_, _) => new EntityImportAnalysisResponseModel(), Map); + } + + private void Map(EntityXmlAnalysis source, EntityImportAnalysisResponseModel target, MapperContext context) + { + target.Key = source.Key; + target.Alias = source.Alias; + target.EntityType = source.EntityType.ToString(); + } +} diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 8263fe9a2f..c8af27d070 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -5057,6 +5057,209 @@ ] } }, + "/umbraco/management/api/v1/document-type/{id}/export": { + "get": { + "tags": [ + "Document Type" + ], + "operationId": "GetDocumentTypeByIdExport", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "string", + "format": "binary" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/document-type/{id}/import": { + "put": { + "tags": [ + "Document Type" + ], + "operationId": "PutDocumentTypeByIdImport", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImportDocumentTypeRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImportDocumentTypeRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImportDocumentTypeRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/document-type/{id}/move": { "put": { "tags": [ @@ -5837,6 +6040,152 @@ ] } }, + "/umbraco/management/api/v1/document-type/import": { + "post": { + "tags": [ + "Document Type" + ], + "operationId": "PostDocumentTypeImport", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImportDocumentTypeRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImportDocumentTypeRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImportDocumentTypeRequestModel" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Umb-Generated-Resource": { + "description": "Identifier of the newly created resource", + "schema": { + "type": "string", + "description": "Identifier of the newly created resource" + } + }, + "Location": { + "description": "Location of the newly created resource", + "schema": { + "type": "string", + "description": "Location of the newly created resource", + "format": "uri" + } + }, + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/item/document-type": { "get": { "tags": [ @@ -10893,6 +11242,76 @@ ] } }, + "/umbraco/management/api/v1/import/analyze": { + "get": { + "tags": [ + "Import" + ], + "operationId": "GetImportAnalyze", + "parameters": [ + { + "name": "temporaryFileId", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/EntityImportAnalysisResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/indexer": { "get": { "tags": [ @@ -13601,6 +14020,209 @@ ] } }, + "/umbraco/management/api/v1/media-type/{id}/export": { + "get": { + "tags": [ + "Media Type" + ], + "operationId": "GetMediaTypeByIdExport", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "string", + "format": "binary" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/media-type/{id}/import": { + "put": { + "tags": [ + "Media Type" + ], + "operationId": "PutMediaTypeByIdImport", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImportMediaTypeRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImportMediaTypeRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImportMediaTypeRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/media-type/{id}/move": { "put": { "tags": [ @@ -14346,6 +14968,152 @@ ] } }, + "/umbraco/management/api/v1/media-type/import": { + "post": { + "tags": [ + "Media Type" + ], + "operationId": "PostMediaTypeImport", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImportMediaTypeRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImportMediaTypeRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImportMediaTypeRequestModel" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Umb-Generated-Resource": { + "description": "Identifier of the newly created resource", + "schema": { + "type": "string", + "description": "Identifier of the newly created resource" + } + }, + "Location": { + "description": "Location of the newly created resource", + "schema": { + "type": "string", + "description": "Location of the newly created resource", + "format": "uri" + } + }, + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/tree/media-type/ancestors": { "get": { "tags": [ @@ -36517,6 +37285,27 @@ }, "additionalProperties": false }, + "EntityImportAnalysisResponseModel": { + "required": [ + "entityType" + ], + "type": "object", + "properties": { + "entityType": { + "type": "string" + }, + "alias": { + "type": "string", + "nullable": true + }, + "key": { + "type": "string", + "format": "uuid", + "nullable": true + } + }, + "additionalProperties": false + }, "EventMessageTypeModel": { "enum": [ "Default", @@ -36863,6 +37652,38 @@ }, "additionalProperties": false }, + "ImportDocumentTypeRequestModel": { + "required": [ + "file" + ], + "type": "object", + "properties": { + "file": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, + "additionalProperties": false + }, + "ImportMediaTypeRequestModel": { + "required": [ + "file" + ], + "type": "object", + "properties": { + "file": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, + "additionalProperties": false + }, "IndexResponseModel": { "required": [ "canRebuild", diff --git a/src/Umbraco.Cms.Api.Management/Services/DictionaryItemImportService.cs b/src/Umbraco.Cms.Api.Management/Services/DictionaryItemImportService.cs index 53d9c4fb5a..12dd1d3593 100644 --- a/src/Umbraco.Cms.Api.Management/Services/DictionaryItemImportService.cs +++ b/src/Umbraco.Cms.Api.Management/Services/DictionaryItemImportService.cs @@ -17,7 +17,7 @@ namespace Umbraco.Cms.Api.Management.Services; internal sealed class DictionaryItemImportService : IDictionaryItemImportService { private readonly IDictionaryItemService _dictionaryItemService; - private readonly PackageDataInstallation _packageDataInstallation; + private readonly IPackageDataInstallation _packageDataInstallation; private readonly ILogger _logger; private readonly ITemporaryFileService _temporaryFileService; private readonly IUserService _userService; @@ -25,7 +25,7 @@ internal sealed class DictionaryItemImportService : IDictionaryItemImportService public DictionaryItemImportService( IDictionaryItemService dictionaryItemService, - PackageDataInstallation packageDataInstallation, + IPackageDataInstallation packageDataInstallation, ILogger logger, ITemporaryFileService temporaryFileService, IUserService userService, diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/ImportDocumentTypeRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/ImportDocumentTypeRequestModel.cs new file mode 100644 index 0000000000..5a2d6e503f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/ImportDocumentTypeRequestModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType; + +public class ImportDocumentTypeRequestModel +{ + public required ReferenceByIdModel File { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Import/EntityImportAnalysisResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Import/EntityImportAnalysisResponseModel.cs new file mode 100644 index 0000000000..122047811d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Import/EntityImportAnalysisResponseModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Import; + +public class EntityImportAnalysisResponseModel +{ + public string EntityType { get; set; } = string.Empty; + + public string? Alias { get; set; } + + public Guid? Key { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/ImportMediaTypeRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/ImportMediaTypeRequestModel.cs new file mode 100644 index 0000000000..a547f69443 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/ImportMediaTypeRequestModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.MediaType; + +public class ImportMediaTypeRequestModel +{ + public required ReferenceByIdModel File { get; set; } +} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 4c4b5d0547..725cacac81 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -37,6 +37,7 @@ using Umbraco.Cms.Core.Services.ContentTypeEditing; using Umbraco.Cms.Core.DynamicRoot; using Umbraco.Cms.Core.Security.Authorization; using Umbraco.Cms.Core.Services.FileSystem; +using Umbraco.Cms.Core.Services.ImportExport; using Umbraco.Cms.Core.Services.Querying.RecycleBin; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Telemetry; @@ -395,6 +396,11 @@ namespace Umbraco.Cms.Core.DependencyInjection // Segments Services.AddUnique(); + + // definition Import/export + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); } } } diff --git a/src/Umbraco.Core/Models/ImportExport/EntityXmlAnalysis.cs b/src/Umbraco.Core/Models/ImportExport/EntityXmlAnalysis.cs new file mode 100644 index 0000000000..d174883018 --- /dev/null +++ b/src/Umbraco.Core/Models/ImportExport/EntityXmlAnalysis.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Core.Models.ImportExport; + +public class EntityXmlAnalysis +{ + public UmbracoEntityTypes EntityType { get; set; } + + public string? Alias { get; set; } + + public Guid? Key { get; set; } +} diff --git a/src/Umbraco.Core/Services/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs index e0b5064a60..b22fcfd46e 100644 --- a/src/Umbraco.Core/Services/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs @@ -396,7 +396,7 @@ internal class EntityXmlSerializer : IEntityXmlSerializer { foreach (ContentTypeSort allowedType in mediaType.AllowedContentTypes) { - structure.Add(new XElement("MediaType", allowedType.Alias)); + structure.Add(new XElement(IEntityXmlSerializer.MediaTypeElementName, allowedType.Alias)); } } @@ -409,7 +409,7 @@ internal class EntityXmlSerializer : IEntityXmlSerializer SerializePropertyGroups(mediaType.PropertyGroups)); // TODO Rename to PropertyGroups var xml = new XElement( - "MediaType", + IEntityXmlSerializer.MediaTypeElementName, info, structure, genericProperties, @@ -474,7 +474,7 @@ internal class EntityXmlSerializer : IEntityXmlSerializer { foreach (ContentTypeSort allowedType in contentType.AllowedContentTypes) { - structure.Add(new XElement("DocumentType", allowedType.Alias)); + structure.Add(new XElement(IEntityXmlSerializer.DocumentTypeElementName, allowedType.Alias)); } } @@ -487,7 +487,7 @@ internal class EntityXmlSerializer : IEntityXmlSerializer SerializePropertyGroups(contentType.PropertyGroups)); // TODO Rename to PropertyGroups var xml = new XElement( - "DocumentType", + IEntityXmlSerializer.DocumentTypeElementName, info, structure, genericProperties, diff --git a/src/Umbraco.Core/Services/IEntityXmlSerializer.cs b/src/Umbraco.Core/Services/IEntityXmlSerializer.cs index 172f710a21..a0549d5d1b 100644 --- a/src/Umbraco.Core/Services/IEntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/IEntityXmlSerializer.cs @@ -8,6 +8,9 @@ namespace Umbraco.Cms.Core.Services; /// public interface IEntityXmlSerializer { + internal const string DocumentTypeElementName = "DocumentType"; + internal const string MediaTypeElementName = "MediaType"; + /// /// Exports an IContent item as an XElement. /// diff --git a/src/Umbraco.Core/Services/IPackageDataInstallation.cs b/src/Umbraco.Core/Services/IPackageDataInstallation.cs new file mode 100644 index 0000000000..27eee95f32 --- /dev/null +++ b/src/Umbraco.Core/Services/IPackageDataInstallation.cs @@ -0,0 +1,80 @@ +using System.Xml.Linq; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Packaging; +using Umbraco.Cms.Core.Packaging; + +namespace Umbraco.Cms.Core.Services; + +public interface IPackageDataInstallation +{ + InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional id of the User performing the operation. Default is zero (admin). + /// An enumerable list of generated ContentTypes + IReadOnlyList ImportMediaTypes(IEnumerable docTypeElements, int userId); + + IReadOnlyList ImportContentBase( + IEnumerable docs, + IDictionary importedDocumentTypes, + int userId, + IContentTypeBaseService typeService, + IContentServiceBase service) + where TContentBase : class, IContentBase + where TContentTypeComposition : IContentTypeComposition; + + IReadOnlyList ImportDocumentType(XElement docTypeElement, int userId); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional id of the User performing the operation. Default is zero (admin). + /// An enumerable list of generated ContentTypes + IReadOnlyList ImportDocumentTypes(IEnumerable docTypeElements, int userId); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional id of the user + /// An enumerable list of generated DataTypeDefinitions + IReadOnlyList ImportDataTypes(IReadOnlyCollection dataTypeElements, int userId); + + /// + /// Imports and saves the 'DictionaryItems' part of the package xml as a list of + /// + /// Xml to import + /// + /// An enumerable list of dictionary items + IReadOnlyList ImportDictionaryItems(IEnumerable dictionaryItemElementList, + int userId); + + IEnumerable ImportDictionaryItem(XElement dictionaryItemElement, int userId, Guid? parentId); + + /// + /// Imports and saves the 'Languages' part of a package xml as a list of + /// + /// Xml to import + /// Optional id of the User performing the operation + /// An enumerable list of generated languages + IReadOnlyList ImportLanguages(IEnumerable languageElements, int userId); + + IEnumerable ImportTemplate(XElement templateElement, int userId); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional user id + /// An enumerable list of generated Templates + IReadOnlyList ImportTemplates(IReadOnlyCollection templateElements, int userId); + + Guid GetContentTypeKey(XElement contentType); + + string? GetEntityTypeAlias(XElement entityType); +} diff --git a/src/Umbraco.Core/Services/ImportExport/ContentTypeImportService.cs b/src/Umbraco.Core/Services/ImportExport/ContentTypeImportService.cs new file mode 100644 index 0000000000..ec9cc5a5ce --- /dev/null +++ b/src/Umbraco.Core/Services/ImportExport/ContentTypeImportService.cs @@ -0,0 +1,93 @@ +using System.Xml.Linq; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services.ImportExport; + +public class ContentTypeImportService : IContentTypeImportService +{ + private readonly IPackageDataInstallation _packageDataInstallation; + private readonly IEntityService _entityService; + private readonly ITemporaryFileToXmlImportService _temporaryFileToXmlImportService; + private readonly ICoreScopeProvider _coreScopeProvider; + private readonly IUserIdKeyResolver _userIdKeyResolver; + + public ContentTypeImportService( + IPackageDataInstallation packageDataInstallation, + IEntityService entityService, + ITemporaryFileToXmlImportService temporaryFileToXmlImportService, + ICoreScopeProvider coreScopeProvider, + IUserIdKeyResolver userIdKeyResolver) + { + _packageDataInstallation = packageDataInstallation; + _entityService = entityService; + _temporaryFileToXmlImportService = temporaryFileToXmlImportService; + _coreScopeProvider = coreScopeProvider; + _userIdKeyResolver = userIdKeyResolver; + } + + /// + /// Imports the contentType + /// + /// + /// + /// the id of the contentType to overwrite, null if a new contentType should be created + /// + public async Task> Import( + Guid temporaryFileId, + Guid userKey, + Guid? contentTypeId = null) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + + _temporaryFileToXmlImportService.CleanupFileIfScopeCompletes(temporaryFileId); + Attempt loadXmlAttempt = + await _temporaryFileToXmlImportService.LoadXElementFromTemporaryFileAsync(temporaryFileId); + if (loadXmlAttempt.Success is false) + { + return Attempt.FailWithStatus( + loadXmlAttempt.Status is TemporaryFileXmlImportOperationStatus.TemporaryFileNotFound + ? ContentTypeImportOperationStatus.TemporaryFileNotFound + : ContentTypeImportOperationStatus.TemporaryFileConversionFailure, + null); + } + + Attempt packageEntityTypeAttempt = _temporaryFileToXmlImportService.GetEntityType(loadXmlAttempt.Result!); + if (packageEntityTypeAttempt.Success is false || + packageEntityTypeAttempt.Result is not UmbracoEntityTypes.DocumentType) + { + return Attempt.FailWithStatus( + ContentTypeImportOperationStatus.TypeMismatch, + null); + } + + Guid packageEntityKey = _packageDataInstallation.GetContentTypeKey(loadXmlAttempt.Result!); + if (contentTypeId is not null && contentTypeId.Equals(packageEntityKey) is false) + { + return Attempt.FailWithStatus( + ContentTypeImportOperationStatus.IdMismatch, + null); + } + + var entityExits = _entityService.Exists(packageEntityKey, UmbracoObjectTypes.DocumentType); + if (entityExits && contentTypeId is null) + { + return Attempt.FailWithStatus( + ContentTypeImportOperationStatus.DocumentTypeExists, + null); + } + + IReadOnlyList importResult = + _packageDataInstallation.ImportDocumentType(loadXmlAttempt.Result!, await _userIdKeyResolver.GetAsync(userKey)); + + scope.Complete(); + + return Attempt.SucceedWithStatus( + entityExits + ? ContentTypeImportOperationStatus.SuccessUpdated + : ContentTypeImportOperationStatus.SuccessCreated, + importResult[0]); + } +} diff --git a/src/Umbraco.Core/Services/ImportExport/IContentTypeImportService.cs b/src/Umbraco.Core/Services/ImportExport/IContentTypeImportService.cs new file mode 100644 index 0000000000..6df7c36d24 --- /dev/null +++ b/src/Umbraco.Core/Services/ImportExport/IContentTypeImportService.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services.ImportExport; + +public interface IContentTypeImportService +{ + Task> Import(Guid temporaryFileId, Guid userKey, Guid? contentTypeId = null); +} diff --git a/src/Umbraco.Core/Services/ImportExport/IMediaTypeImportService.cs b/src/Umbraco.Core/Services/ImportExport/IMediaTypeImportService.cs new file mode 100644 index 0000000000..a23e07dfc8 --- /dev/null +++ b/src/Umbraco.Core/Services/ImportExport/IMediaTypeImportService.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services.ImportExport; + +public interface IMediaTypeImportService +{ + Task> Import( + Guid temporaryFileId, + Guid userKey, + Guid? mediaTypeId = null); +} diff --git a/src/Umbraco.Core/Services/ImportExport/ITemporaryFileToXmlImportService.cs b/src/Umbraco.Core/Services/ImportExport/ITemporaryFileToXmlImportService.cs new file mode 100644 index 0000000000..5f2bf25c62 --- /dev/null +++ b/src/Umbraco.Core/Services/ImportExport/ITemporaryFileToXmlImportService.cs @@ -0,0 +1,19 @@ +using System.Xml.Linq; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.ImportExport; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services.ImportExport; + +public interface ITemporaryFileToXmlImportService +{ + Task> LoadXElementFromTemporaryFileAsync( + Guid temporaryFileId); + + Attempt GetEntityType(XElement entityElement); + + Task> AnalyzeAsync( + Guid temporaryFileId); + + void CleanupFileIfScopeCompletes(Guid temporaryFileId); +} diff --git a/src/Umbraco.Core/Services/ImportExport/MediaTypeImportService.cs b/src/Umbraco.Core/Services/ImportExport/MediaTypeImportService.cs new file mode 100644 index 0000000000..09fdc0cb27 --- /dev/null +++ b/src/Umbraco.Core/Services/ImportExport/MediaTypeImportService.cs @@ -0,0 +1,88 @@ +using System.Xml.Linq; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services.ImportExport; + +public class MediaTypeImportService : IMediaTypeImportService +{ + private readonly IPackageDataInstallation _packageDataInstallation; + private readonly IEntityService _entityService; + private readonly ITemporaryFileToXmlImportService _temporaryFileToXmlImportService; + private readonly ICoreScopeProvider _coreScopeProvider; + private readonly IUserIdKeyResolver _userIdKeyResolver; + + public MediaTypeImportService( + IPackageDataInstallation packageDataInstallation, + IEntityService entityService, + ITemporaryFileToXmlImportService temporaryFileToXmlImportService, + ICoreScopeProvider coreScopeProvider, + IUserIdKeyResolver userIdKeyResolver) + { + _packageDataInstallation = packageDataInstallation; + _entityService = entityService; + _temporaryFileToXmlImportService = temporaryFileToXmlImportService; + _coreScopeProvider = coreScopeProvider; + _userIdKeyResolver = userIdKeyResolver; + } + + public async Task> Import( + Guid temporaryFileId, + Guid userKey, + Guid? mediaTypeId = null) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + + _temporaryFileToXmlImportService.CleanupFileIfScopeCompletes(temporaryFileId); + Attempt loadXmlAttempt = + await _temporaryFileToXmlImportService.LoadXElementFromTemporaryFileAsync(temporaryFileId); + if (loadXmlAttempt.Success is false) + { + return Attempt.FailWithStatus( + loadXmlAttempt.Status is TemporaryFileXmlImportOperationStatus.TemporaryFileNotFound + ? MediaTypeImportOperationStatus.TemporaryFileNotFound + : MediaTypeImportOperationStatus.TemporaryFileConversionFailure, + null); + } + + Attempt packageEntityTypeAttempt = _temporaryFileToXmlImportService.GetEntityType(loadXmlAttempt.Result!); + if (packageEntityTypeAttempt.Success is false || + packageEntityTypeAttempt.Result is not UmbracoEntityTypes.MediaType) + { + return Attempt.FailWithStatus( + MediaTypeImportOperationStatus.TypeMismatch, + null); + } + + Guid packageEntityKey = _packageDataInstallation.GetContentTypeKey(loadXmlAttempt.Result!); + if (mediaTypeId is not null && mediaTypeId.Equals(packageEntityKey) is false) + { + return Attempt.FailWithStatus( + MediaTypeImportOperationStatus.IdMismatch, + null); + } + + var entityExits = _entityService.Exists( + _packageDataInstallation.GetContentTypeKey(loadXmlAttempt.Result!), + UmbracoObjectTypes.MediaType); + if (entityExits && mediaTypeId is null) + { + return Attempt.FailWithStatus( + MediaTypeImportOperationStatus.MediaTypeExists, + null); + } + + IReadOnlyList importResult = + _packageDataInstallation.ImportMediaTypes(new[] { loadXmlAttempt.Result! }, await _userIdKeyResolver.GetAsync(userKey)); + + scope.Complete(); + + return Attempt.SucceedWithStatus( + entityExits + ? MediaTypeImportOperationStatus.SuccessUpdated + : MediaTypeImportOperationStatus.SuccessCreated, + importResult[0]); + } +} diff --git a/src/Umbraco.Core/Services/ImportExport/TemporaryFileToXmlImportService.cs b/src/Umbraco.Core/Services/ImportExport/TemporaryFileToXmlImportService.cs new file mode 100644 index 0000000000..b8621a5044 --- /dev/null +++ b/src/Umbraco.Core/Services/ImportExport/TemporaryFileToXmlImportService.cs @@ -0,0 +1,100 @@ +using System.Xml; +using System.Xml.Linq; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.ImportExport; +using Umbraco.Cms.Core.Models.TemporaryFile; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services.ImportExport; + +public class TemporaryFileToXmlImportService : ITemporaryFileToXmlImportService +{ + private readonly ITemporaryFileService _temporaryFileService; + private readonly IPackageDataInstallation _packageDataInstallation; + private readonly ICoreScopeProvider _coreScopeProvider; + + public TemporaryFileToXmlImportService( + ITemporaryFileService temporaryFileService, + IPackageDataInstallation packageDataInstallation, + ICoreScopeProvider coreScopeProvider) + { + _temporaryFileService = temporaryFileService; + _packageDataInstallation = packageDataInstallation; + _coreScopeProvider = coreScopeProvider; + } + + /// + /// Only if this method is called within a scope, the temporary file will be cleaned up if that scope completes. + /// + public async Task> LoadXElementFromTemporaryFileAsync( + Guid temporaryFileId) + { + TemporaryFileModel? documentTypeFile = await _temporaryFileService.GetAsync(temporaryFileId); + if (documentTypeFile is null) + { + return Attempt.FailWithStatus( + TemporaryFileXmlImportOperationStatus.TemporaryFileNotFound, null); + } + + XDocument document; + await using (Stream fileStream = documentTypeFile.OpenReadStream()) + { + document = await XDocument.LoadAsync(fileStream, LoadOptions.None, CancellationToken.None); + } + + return Attempt.SucceedWithStatus( + TemporaryFileXmlImportOperationStatus.Success, + document.Root); + } + + public void CleanupFileIfScopeCompletes(Guid temporaryFileId) + => _temporaryFileService.EnlistDeleteIfScopeCompletes(temporaryFileId, _coreScopeProvider); + + public Attempt GetEntityType(XElement entityElement) + { + var entityType = entityElement.Name.ToString(); + return entityType switch + { + IEntityXmlSerializer.DocumentTypeElementName + => Attempt.Succeed(UmbracoEntityTypes.DocumentType), + IEntityXmlSerializer.MediaTypeElementName + => Attempt.Succeed(UmbracoEntityTypes.MediaType), + _ => Attempt.Fail() + }; + } + + /// + /// Reads the file trough the use of and returns basic information regarding the entity that would be imported if this file was processed by + /// or . + /// + /// As this method does not persist anything, no scope is created and the temporary file is not cleaned up, see remark in . + public async Task> AnalyzeAsync( + Guid temporaryFileId) + { + Attempt xmlElementAttempt = + await LoadXElementFromTemporaryFileAsync(temporaryFileId); + + if (xmlElementAttempt.Success is false) + { + return Attempt.Fail(xmlElementAttempt.Status); + } + + Attempt entityTypeAttempt = GetEntityType(xmlElementAttempt.Result!); + if (entityTypeAttempt.Success is false) + { + return Attempt.Fail( + TemporaryFileXmlImportOperationStatus.UndeterminedEntityType); + } + + Guid entityTypeKey = _packageDataInstallation.GetContentTypeKey(xmlElementAttempt.Result!); + var entityTypeAlias = _packageDataInstallation.GetEntityTypeAlias(xmlElementAttempt.Result!); + return Attempt.Succeed( + TemporaryFileXmlImportOperationStatus.Success, + new EntityXmlAnalysis + { + EntityType = entityTypeAttempt.Result, Alias = entityTypeAlias, Key = entityTypeKey, + }); + } +} diff --git a/src/Umbraco.Core/Services/OperationStatus/ContentTypeImportOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/ContentTypeImportOperationStatus.cs new file mode 100644 index 0000000000..93fb9b3904 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/ContentTypeImportOperationStatus.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum ContentTypeImportOperationStatus +{ + SuccessCreated, + SuccessUpdated, + TemporaryFileNotFound, + TemporaryFileConversionFailure, + DocumentTypeExists, + IdMismatch, + TypeMismatch, +} diff --git a/src/Umbraco.Core/Services/OperationStatus/MediaTypeImportOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/MediaTypeImportOperationStatus.cs new file mode 100644 index 0000000000..2ca85e832b --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/MediaTypeImportOperationStatus.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum MediaTypeImportOperationStatus +{ + SuccessCreated, + SuccessUpdated, + TemporaryFileNotFound, + TemporaryFileConversionFailure, + MediaTypeExists, + TypeMismatch, + IdMismatch +} diff --git a/src/Umbraco.Core/Services/OperationStatus/TemporaryFileXmlImportOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/TemporaryFileXmlImportOperationStatus.cs new file mode 100644 index 0000000000..1312e030e5 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/TemporaryFileXmlImportOperationStatus.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum TemporaryFileXmlImportOperationStatus +{ + Success = 0, + TemporaryFileNotFound, + UndeterminedEntityType, +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 19dd32eb43..b7bc79997f 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -98,8 +98,8 @@ public static partial class UmbracoBuilderExtensions packageRepoFileName); // Factory registration is only required because of ambiguous constructor - private static PackageDataInstallation CreatePackageDataInstallation(IServiceProvider factory) - => new( + private static IPackageDataInstallation CreatePackageDataInstallation(IServiceProvider factory) + => new PackageDataInstallation( factory.GetRequiredService(), factory.GetRequiredService>(), factory.GetRequiredService(), diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index 2855b021f6..32be1b1cc1 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -21,7 +21,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Packaging { - public class PackageDataInstallation + public class PackageDataInstallation : IPackageDataInstallation { private readonly IDataValueEditorFactory _dataValueEditorFactory; private readonly ILogger _logger; @@ -590,7 +590,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging //Iterate the sorted document types and create them as IContentType objects foreach (XElement documentType in documentTypes) { - var alias = documentType.Element("Info")?.Element("Alias")?.Value; + var alias = GetEntityTypeAlias(documentType); if (alias is not null && importedContentTypes.ContainsKey(alias) == false) { @@ -623,7 +623,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging //Update the structure here - we can't do it until all DocTypes have been created foreach (XElement documentType in documentTypes) { - var alias = documentType.Element("Info")?.Element("Alias")?.Value; + var alias = GetEntityTypeAlias(documentType); XElement? structureElement = documentType.Element("Structure"); //Ensure that we only update ContentTypes which has actual structure-elements if (structureElement == null || structureElement.Elements().Any() == false || alias is null) @@ -661,7 +661,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging // exist which contains it's folders && ((string?)infoElement.Element("Master")).IsNullOrWhiteSpace()) { - var alias = documentType.Element("Info")?.Element("Alias")?.Value; + var alias = GetEntityTypeAlias(documentType); var folders = foldersAttribute.Value.Split(Constants.CharArrays.ForwardSlash); XAttribute? folderKeysAttribute = documentType.Attribute("FolderKeys"); @@ -744,13 +744,19 @@ namespace Umbraco.Cms.Infrastructure.Packaging return _contentTypeService.GetContainer(tryCreateFolder.Result!.Entity!.Id); } + public Guid GetContentTypeKey(XElement contentType) + => Guid.Parse(contentType.Element("Info")!.Element("Key")!.Value); + + public string? GetEntityTypeAlias(XElement entityType) + => entityType.Element("Info")?.Element("Alias")?.Value; + private T CreateContentTypeFromXml( XElement documentType, IReadOnlyDictionary importedContentTypes, IContentTypeBaseService service) where T : class, IContentTypeComposition { - var key = Guid.Parse(documentType.Element("Info")!.Element("Key")!.Value); + var key = GetContentTypeKey(documentType); XElement infoElement = documentType.Element("Info")!; diff --git a/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs index 6d572cb29c..b7b3df2492 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs @@ -2,19 +2,20 @@ using System.Xml.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Packaging; using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Services; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Packaging; public class PackageInstallation : IPackageInstallation { - private readonly PackageDataInstallation _packageDataInstallation; + private readonly IPackageDataInstallation _packageDataInstallation; private readonly CompiledPackageXmlParser _parser; /// /// Initializes a new instance of the class. /// - public PackageInstallation(PackageDataInstallation packageDataInstallation, CompiledPackageXmlParser parser) + public PackageInstallation(IPackageDataInstallation packageDataInstallation, CompiledPackageXmlParser parser) { _packageDataInstallation = packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation)); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/PackageDataInstallationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/PackageDataInstallationTests.cs index 5fe47f4d47..8bf05e633d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/PackageDataInstallationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/PackageDataInstallationTests.cs @@ -73,7 +73,7 @@ public class PackageDataInstallationTests : UmbracoIntegrationTestWithContent //// Builder.ComposeFileSystems(); //// } - private PackageDataInstallation PackageDataInstallation => GetRequiredService(); + private IPackageDataInstallation PackageDataInstallation => GetRequiredService(); private IMediaService MediaService => GetRequiredService();