[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 <sge@umbraco.dk>
Co-authored-by: kjac <kja@umbraco.dk>
This commit is contained in:
Sven Geusens
2024-05-17 14:35:18 +02:00
committed by GitHub
parent 13f2d52576
commit 80794f3efd
39 changed files with 1795 additions and 17 deletions

View File

@@ -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
{

View File

@@ -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);
}
}

View File

@@ -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<IActionResult> Import(
CancellationToken cancellationToken,
Guid id,
ImportDocumentTypeRequestModel model)
{
Attempt<IContentType?, ContentTypeImportOperationStatus> importAttempt = await _contentTypeImportService.Import(model.File.Id, CurrentUserKey(_backOfficeSecurityAccessor), id);
return importAttempt.Success is false
? ContentTypeImportOperationStatusResult(importAttempt.Status)
: Ok();
}
}

View File

@@ -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<IActionResult> Import(
CancellationToken cancellationToken,
ImportDocumentTypeRequestModel model)
{
Attempt<IContentType?, ContentTypeImportOperationStatus> importAttempt = await _contentTypeImportService.Import(model.File.Id, CurrentUserKey(_backOfficeSecurityAccessor));
return importAttempt.Success is false
? ContentTypeImportOperationStatusResult(importAttempt.Status)
: CreatedAtId<ByKeyDocumentTypeController>(
controller => nameof(controller.ByKey),
importAttempt.Result!.Key);
}
}

View File

@@ -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<IActionResult> Analyze(CancellationToken cancellationToken, Guid temporaryFileId)
{
Attempt<EntityXmlAnalysis?, TemporaryFileXmlImportOperationStatus> analyzeResult = await _temporaryFileToXmlImportService.AnalyzeAsync(temporaryFileId);
return analyzeResult.Success is false
? TemporaryFileXmlImportOperationStatusResult(analyzeResult.Status)
: Ok(_mapper.Map<EntityImportAnalysisResponseModel>(analyzeResult.Result));
}
}

View File

@@ -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 },
});
}

View File

@@ -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);
}
}

View File

@@ -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<IActionResult> Import(
CancellationToken cancellationToken,
Guid id,
ImportMediaTypeRequestModel model)
{
Attempt<IMediaType?, MediaTypeImportOperationStatus> importAttempt = await _mediaTypeImportService.Import(model.File.Id, CurrentUserKey(_backOfficeSecurityAccessor));
return importAttempt.Success is false
? MediaTypeImportOperationStatusResult(importAttempt.Status)
: Ok();
}
}

View File

@@ -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<IActionResult> Import(
CancellationToken cancellationToken,
ImportMediaTypeRequestModel model)
{
Attempt<IMediaType?, MediaTypeImportOperationStatus> importAttempt = await _mediaTypeImportService.Import(model.File.Id, CurrentUserKey(_backOfficeSecurityAccessor));
return importAttempt.Success is false
? MediaTypeImportOperationStatusResult(importAttempt.Status)
: CreatedAtId<ByKeyMediaTypeController>(controller => nameof(controller.ByKey), importAttempt.Result!.Key);
}
}

View File

@@ -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.")
});
}

View File

@@ -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<IUdtFileContentFactory, UdtFileContentFactory>();
return builder;
}
}

View File

@@ -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<MapDefinitionCollectionBuilder>()
.Add<EntityImportAnalysisViewModelsMapDefinition>();
return builder;
}
}

View File

@@ -68,7 +68,9 @@ public static partial class UmbracoBuilderExtensions
.AddPasswordConfiguration()
.AddSupplemenataryLocalizedTextFileSources()
.AddUserData()
.AddSegment();
.AddSegment()
.AddExport()
.AddImport();
services
.ConfigureOptions<ConfigureApiBehaviorOptions>()

View File

@@ -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);
}

View File

@@ -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"
};
}

View File

@@ -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<EntityXmlAnalysis, EntityImportAnalysisResponseModel>((_, _) => 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();
}
}

View File

@@ -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",

View File

@@ -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<DictionaryItemImportService> _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<DictionaryItemImportService> logger,
ITemporaryFileService temporaryFileService,
IUserService userService,

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType;
public class ImportDocumentTypeRequestModel
{
public required ReferenceByIdModel File { get; set; }
}

View File

@@ -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; }
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.MediaType;
public class ImportMediaTypeRequestModel
{
public required ReferenceByIdModel File { get; set; }
}

View File

@@ -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<ISegmentService, NoopSegmentService>();
// definition Import/export
Services.AddUnique<ITemporaryFileToXmlImportService, TemporaryFileToXmlImportService>();
Services.AddUnique<IContentTypeImportService, ContentTypeImportService>();
Services.AddUnique<IMediaTypeImportService, MediaTypeImportService>();
}
}
}

View File

@@ -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; }
}

View File

@@ -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,

View File

@@ -8,6 +8,9 @@ namespace Umbraco.Cms.Core.Services;
/// </summary>
public interface IEntityXmlSerializer
{
internal const string DocumentTypeElementName = "DocumentType";
internal const string MediaTypeElementName = "MediaType";
/// <summary>
/// Exports an IContent item as an XElement.
/// </summary>

View File

@@ -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);
/// <summary>
/// Imports and saves package xml as <see cref="IContentType"/>
/// </summary>
/// <param name="docTypeElements">Xml to import</param>
/// <param name="userId">Optional id of the User performing the operation. Default is zero (admin).</param>
/// <returns>An enumerable list of generated ContentTypes</returns>
IReadOnlyList<IMediaType> ImportMediaTypes(IEnumerable<XElement> docTypeElements, int userId);
IReadOnlyList<TContentBase> ImportContentBase<TContentBase, TContentTypeComposition>(
IEnumerable<CompiledPackageContentBase> docs,
IDictionary<string, TContentTypeComposition> importedDocumentTypes,
int userId,
IContentTypeBaseService<TContentTypeComposition> typeService,
IContentServiceBase<TContentBase> service)
where TContentBase : class, IContentBase
where TContentTypeComposition : IContentTypeComposition;
IReadOnlyList<IContentType> ImportDocumentType(XElement docTypeElement, int userId);
/// <summary>
/// Imports and saves package xml as <see cref="IContentType"/>
/// </summary>
/// <param name="docTypeElements">Xml to import</param>
/// <param name="userId">Optional id of the User performing the operation. Default is zero (admin).</param>
/// <returns>An enumerable list of generated ContentTypes</returns>
IReadOnlyList<IContentType> ImportDocumentTypes(IEnumerable<XElement> docTypeElements, int userId);
/// <summary>
/// Imports and saves package xml as <see cref="IDataType"/>
/// </summary>
/// <param name="dataTypeElements">Xml to import</param>
/// <param name="userId">Optional id of the user</param>
/// <returns>An enumerable list of generated DataTypeDefinitions</returns>
IReadOnlyList<IDataType> ImportDataTypes(IReadOnlyCollection<XElement> dataTypeElements, int userId);
/// <summary>
/// Imports and saves the 'DictionaryItems' part of the package xml as a list of <see cref="IDictionaryItem"/>
/// </summary>
/// <param name="dictionaryItemElementList">Xml to import</param>
/// <param name="userId"></param>
/// <returns>An enumerable list of dictionary items</returns>
IReadOnlyList<IDictionaryItem> ImportDictionaryItems(IEnumerable<XElement> dictionaryItemElementList,
int userId);
IEnumerable<IDictionaryItem> ImportDictionaryItem(XElement dictionaryItemElement, int userId, Guid? parentId);
/// <summary>
/// Imports and saves the 'Languages' part of a package xml as a list of <see cref="ILanguage"/>
/// </summary>
/// <param name="languageElements">Xml to import</param>
/// <param name="userId">Optional id of the User performing the operation</param>
/// <returns>An enumerable list of generated languages</returns>
IReadOnlyList<ILanguage> ImportLanguages(IEnumerable<XElement> languageElements, int userId);
IEnumerable<ITemplate> ImportTemplate(XElement templateElement, int userId);
/// <summary>
/// Imports and saves package xml as <see cref="ITemplate"/>
/// </summary>
/// <param name="templateElements">Xml to import</param>
/// <param name="userId">Optional user id</param>
/// <returns>An enumerable list of generated Templates</returns>
IReadOnlyList<ITemplate> ImportTemplates(IReadOnlyCollection<XElement> templateElements, int userId);
Guid GetContentTypeKey(XElement contentType);
string? GetEntityTypeAlias(XElement entityType);
}

View File

@@ -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;
}
/// <summary>
/// Imports the contentType
/// </summary>
/// <param name="temporaryFileId"></param>
/// <param name="userId"></param>
/// <param name="contentTypeId">the id of the contentType to overwrite, null if a new contentType should be created</param>
/// <returns></returns>
public async Task<Attempt<IContentType?, ContentTypeImportOperationStatus>> Import(
Guid temporaryFileId,
Guid userKey,
Guid? contentTypeId = null)
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
_temporaryFileToXmlImportService.CleanupFileIfScopeCompletes(temporaryFileId);
Attempt<XElement?, TemporaryFileXmlImportOperationStatus> loadXmlAttempt =
await _temporaryFileToXmlImportService.LoadXElementFromTemporaryFileAsync(temporaryFileId);
if (loadXmlAttempt.Success is false)
{
return Attempt.FailWithStatus<IContentType?, ContentTypeImportOperationStatus>(
loadXmlAttempt.Status is TemporaryFileXmlImportOperationStatus.TemporaryFileNotFound
? ContentTypeImportOperationStatus.TemporaryFileNotFound
: ContentTypeImportOperationStatus.TemporaryFileConversionFailure,
null);
}
Attempt<UmbracoEntityTypes> packageEntityTypeAttempt = _temporaryFileToXmlImportService.GetEntityType(loadXmlAttempt.Result!);
if (packageEntityTypeAttempt.Success is false ||
packageEntityTypeAttempt.Result is not UmbracoEntityTypes.DocumentType)
{
return Attempt.FailWithStatus<IContentType?, ContentTypeImportOperationStatus>(
ContentTypeImportOperationStatus.TypeMismatch,
null);
}
Guid packageEntityKey = _packageDataInstallation.GetContentTypeKey(loadXmlAttempt.Result!);
if (contentTypeId is not null && contentTypeId.Equals(packageEntityKey) is false)
{
return Attempt.FailWithStatus<IContentType?, ContentTypeImportOperationStatus>(
ContentTypeImportOperationStatus.IdMismatch,
null);
}
var entityExits = _entityService.Exists(packageEntityKey, UmbracoObjectTypes.DocumentType);
if (entityExits && contentTypeId is null)
{
return Attempt.FailWithStatus<IContentType?, ContentTypeImportOperationStatus>(
ContentTypeImportOperationStatus.DocumentTypeExists,
null);
}
IReadOnlyList<IContentType> importResult =
_packageDataInstallation.ImportDocumentType(loadXmlAttempt.Result!, await _userIdKeyResolver.GetAsync(userKey));
scope.Complete();
return Attempt.SucceedWithStatus<IContentType?, ContentTypeImportOperationStatus>(
entityExits
? ContentTypeImportOperationStatus.SuccessUpdated
: ContentTypeImportOperationStatus.SuccessCreated,
importResult[0]);
}
}

View File

@@ -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<Attempt<IContentType?, ContentTypeImportOperationStatus>> Import(Guid temporaryFileId, Guid userKey, Guid? contentTypeId = null);
}

View File

@@ -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<Attempt<IMediaType?, MediaTypeImportOperationStatus>> Import(
Guid temporaryFileId,
Guid userKey,
Guid? mediaTypeId = null);
}

View File

@@ -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<Attempt<XElement?, TemporaryFileXmlImportOperationStatus>> LoadXElementFromTemporaryFileAsync(
Guid temporaryFileId);
Attempt<UmbracoEntityTypes> GetEntityType(XElement entityElement);
Task<Attempt<EntityXmlAnalysis?, TemporaryFileXmlImportOperationStatus>> AnalyzeAsync(
Guid temporaryFileId);
void CleanupFileIfScopeCompletes(Guid temporaryFileId);
}

View File

@@ -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<Attempt<IMediaType?, MediaTypeImportOperationStatus>> Import(
Guid temporaryFileId,
Guid userKey,
Guid? mediaTypeId = null)
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
_temporaryFileToXmlImportService.CleanupFileIfScopeCompletes(temporaryFileId);
Attempt<XElement?, TemporaryFileXmlImportOperationStatus> loadXmlAttempt =
await _temporaryFileToXmlImportService.LoadXElementFromTemporaryFileAsync(temporaryFileId);
if (loadXmlAttempt.Success is false)
{
return Attempt.FailWithStatus<IMediaType?, MediaTypeImportOperationStatus>(
loadXmlAttempt.Status is TemporaryFileXmlImportOperationStatus.TemporaryFileNotFound
? MediaTypeImportOperationStatus.TemporaryFileNotFound
: MediaTypeImportOperationStatus.TemporaryFileConversionFailure,
null);
}
Attempt<UmbracoEntityTypes> packageEntityTypeAttempt = _temporaryFileToXmlImportService.GetEntityType(loadXmlAttempt.Result!);
if (packageEntityTypeAttempt.Success is false ||
packageEntityTypeAttempt.Result is not UmbracoEntityTypes.MediaType)
{
return Attempt.FailWithStatus<IMediaType?, MediaTypeImportOperationStatus>(
MediaTypeImportOperationStatus.TypeMismatch,
null);
}
Guid packageEntityKey = _packageDataInstallation.GetContentTypeKey(loadXmlAttempt.Result!);
if (mediaTypeId is not null && mediaTypeId.Equals(packageEntityKey) is false)
{
return Attempt.FailWithStatus<IMediaType?, MediaTypeImportOperationStatus>(
MediaTypeImportOperationStatus.IdMismatch,
null);
}
var entityExits = _entityService.Exists(
_packageDataInstallation.GetContentTypeKey(loadXmlAttempt.Result!),
UmbracoObjectTypes.MediaType);
if (entityExits && mediaTypeId is null)
{
return Attempt.FailWithStatus<IMediaType?, MediaTypeImportOperationStatus>(
MediaTypeImportOperationStatus.MediaTypeExists,
null);
}
IReadOnlyList<IMediaType> importResult =
_packageDataInstallation.ImportMediaTypes(new[] { loadXmlAttempt.Result! }, await _userIdKeyResolver.GetAsync(userKey));
scope.Complete();
return Attempt.SucceedWithStatus<IMediaType?, MediaTypeImportOperationStatus>(
entityExits
? MediaTypeImportOperationStatus.SuccessUpdated
: MediaTypeImportOperationStatus.SuccessCreated,
importResult[0]);
}
}

View File

@@ -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;
}
/// <remark>
/// Only if this method is called within a scope, the temporary file will be cleaned up if that scope completes.
/// </remark>
public async Task<Attempt<XElement?, TemporaryFileXmlImportOperationStatus>> LoadXElementFromTemporaryFileAsync(
Guid temporaryFileId)
{
TemporaryFileModel? documentTypeFile = await _temporaryFileService.GetAsync(temporaryFileId);
if (documentTypeFile is null)
{
return Attempt.FailWithStatus<XElement?, TemporaryFileXmlImportOperationStatus>(
TemporaryFileXmlImportOperationStatus.TemporaryFileNotFound, null);
}
XDocument document;
await using (Stream fileStream = documentTypeFile.OpenReadStream())
{
document = await XDocument.LoadAsync(fileStream, LoadOptions.None, CancellationToken.None);
}
return Attempt.SucceedWithStatus<XElement?, TemporaryFileXmlImportOperationStatus>(
TemporaryFileXmlImportOperationStatus.Success,
document.Root);
}
public void CleanupFileIfScopeCompletes(Guid temporaryFileId)
=> _temporaryFileService.EnlistDeleteIfScopeCompletes(temporaryFileId, _coreScopeProvider);
public Attempt<UmbracoEntityTypes> GetEntityType(XElement entityElement)
{
var entityType = entityElement.Name.ToString();
return entityType switch
{
IEntityXmlSerializer.DocumentTypeElementName
=> Attempt<UmbracoEntityTypes>.Succeed(UmbracoEntityTypes.DocumentType),
IEntityXmlSerializer.MediaTypeElementName
=> Attempt<UmbracoEntityTypes>.Succeed(UmbracoEntityTypes.MediaType),
_ => Attempt<UmbracoEntityTypes>.Fail()
};
}
/// <summary>
/// Reads the file trough the use of <see cref="LoadXElementFromTemporaryFileAsync"/> and returns basic information regarding the entity that would be imported if this file was processed by
/// <see cref="IContentTypeImportService.Import(Guid,Guid,System.Nullable{System.Guid})"/> or <see cref="IMediaTypeImportService.Import(Guid,Guid,System.Nullable{System.Guid})"/>.
/// </summary>
/// <remarks>As this method does not persist anything, no scope is created and the temporary file is not cleaned up, see remark in <see cref="LoadXElementFromTemporaryFileAsync"/>.</remarks>
public async Task<Attempt<EntityXmlAnalysis?, TemporaryFileXmlImportOperationStatus>> AnalyzeAsync(
Guid temporaryFileId)
{
Attempt<XElement?, TemporaryFileXmlImportOperationStatus> xmlElementAttempt =
await LoadXElementFromTemporaryFileAsync(temporaryFileId);
if (xmlElementAttempt.Success is false)
{
return Attempt<EntityXmlAnalysis, TemporaryFileXmlImportOperationStatus>.Fail(xmlElementAttempt.Status);
}
Attempt<UmbracoEntityTypes> entityTypeAttempt = GetEntityType(xmlElementAttempt.Result!);
if (entityTypeAttempt.Success is false)
{
return Attempt<EntityXmlAnalysis, TemporaryFileXmlImportOperationStatus>.Fail(
TemporaryFileXmlImportOperationStatus.UndeterminedEntityType);
}
Guid entityTypeKey = _packageDataInstallation.GetContentTypeKey(xmlElementAttempt.Result!);
var entityTypeAlias = _packageDataInstallation.GetEntityTypeAlias(xmlElementAttempt.Result!);
return Attempt<EntityXmlAnalysis?, TemporaryFileXmlImportOperationStatus>.Succeed(
TemporaryFileXmlImportOperationStatus.Success,
new EntityXmlAnalysis
{
EntityType = entityTypeAttempt.Result, Alias = entityTypeAlias, Key = entityTypeKey,
});
}
}

View File

@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Core.Services.OperationStatus;
public enum ContentTypeImportOperationStatus
{
SuccessCreated,
SuccessUpdated,
TemporaryFileNotFound,
TemporaryFileConversionFailure,
DocumentTypeExists,
IdMismatch,
TypeMismatch,
}

View File

@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Core.Services.OperationStatus;
public enum MediaTypeImportOperationStatus
{
SuccessCreated,
SuccessUpdated,
TemporaryFileNotFound,
TemporaryFileConversionFailure,
MediaTypeExists,
TypeMismatch,
IdMismatch
}

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Core.Services.OperationStatus;
public enum TemporaryFileXmlImportOperationStatus
{
Success = 0,
TemporaryFileNotFound,
UndeterminedEntityType,
}

View File

@@ -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<IDataValueEditorFactory>(),
factory.GetRequiredService<ILogger<PackageDataInstallation>>(),
factory.GetRequiredService<IFileService>(),

View File

@@ -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<PackageDataInstallation> _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<T>(
XElement documentType,
IReadOnlyDictionary<string, T> importedContentTypes,
IContentTypeBaseService<T> service)
where T : class, IContentTypeComposition
{
var key = Guid.Parse(documentType.Element("Info")!.Element("Key")!.Value);
var key = GetContentTypeKey(documentType);
XElement infoElement = documentType.Element("Info")!;

View File

@@ -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;
/// <summary>
/// Initializes a new instance of the <see cref="PackageInstallation" /> class.
/// </summary>
public PackageInstallation(PackageDataInstallation packageDataInstallation, CompiledPackageXmlParser parser)
public PackageInstallation(IPackageDataInstallation packageDataInstallation, CompiledPackageXmlParser parser)
{
_packageDataInstallation =
packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation));

View File

@@ -73,7 +73,7 @@ public class PackageDataInstallationTests : UmbracoIntegrationTestWithContent
//// Builder.ComposeFileSystems();
//// }
private PackageDataInstallation PackageDataInstallation => GetRequiredService<PackageDataInstallation>();
private IPackageDataInstallation PackageDataInstallation => GetRequiredService<IPackageDataInstallation>();
private IMediaService MediaService => GetRequiredService<IMediaService>();