Refactor dictionary bulk action APIs (#13786)
* Sanitize dictionary overview and export actions * Amend dictionary services with async and attempt pattern + isolate temporary file handling in its own service. * Update OpenAPI schema to match new dictionary bulk actions * Update src/Umbraco.Cms.Api.Management/Services/DictionaryItemImportService.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Cms.Api.Management/Services/DictionaryItemImportService.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Cms.Api.Management/Services/TemporaryFileService.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryImportModel.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Refactor - split dictionary import handling into smaller methods * Rename view model due to name clash in new SwaggerGen + update OpenAPI JSON file --------- Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
This commit is contained in:
@@ -22,42 +22,15 @@ public class AllDictionaryController : DictionaryControllerBase
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<DictionaryOverviewViewModel>), StatusCodes.Status200OK)]
|
||||
// FIXME: make this action slim (move logic somewhere else)
|
||||
public async Task<ActionResult<PagedViewModel<DictionaryOverviewViewModel>>> All(int skip = 0, int take = 100)
|
||||
{
|
||||
// unfortunately we can't paginate here...we'll have to get all and paginate in memory
|
||||
IDictionaryItem[] items = (await _dictionaryItemService.GetDescendantsAsync(null)).ToArray();
|
||||
var list = new List<DictionaryOverviewViewModel>(items.Length);
|
||||
|
||||
// Build the proper tree structure, as we can have nested dictionary items
|
||||
BuildTree(list, items);
|
||||
|
||||
var model = new PagedViewModel<DictionaryOverviewViewModel>
|
||||
{
|
||||
Total = list.Count,
|
||||
Items = list.Skip(skip).Take(take),
|
||||
Total = items.Length,
|
||||
Items = _umbracoMapper.MapEnumerable<IDictionaryItem, DictionaryOverviewViewModel>(items.Skip(skip).Take(take))
|
||||
};
|
||||
return await Task.FromResult(model);
|
||||
}
|
||||
|
||||
// recursive method to build a tree structure from the flat structure returned above
|
||||
private void BuildTree(List<DictionaryOverviewViewModel> list, IDictionaryItem[] items, int level = 0, Guid? parentId = null)
|
||||
{
|
||||
IDictionaryItem[] children = items.Where(t => t.ParentId == parentId).ToArray();
|
||||
if (children.Any() == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (IDictionaryItem child in children.OrderBy(item => item.ItemKey))
|
||||
{
|
||||
DictionaryOverviewViewModel? display = _umbracoMapper.Map<IDictionaryItem, DictionaryOverviewViewModel>(child);
|
||||
if (display is not null)
|
||||
{
|
||||
display.Level = level;
|
||||
list.Add(display);
|
||||
}
|
||||
|
||||
BuildTree(list, items, level + 1, child.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,23 +11,22 @@ namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
public class ExportDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
// FIXME: use IDictionaryItemService instead of ILocalizationService
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IDictionaryItemService _dictionaryItemService;
|
||||
private readonly IEntityXmlSerializer _entityXmlSerializer;
|
||||
|
||||
public ExportDictionaryController(ILocalizationService localizationService, IEntityXmlSerializer entityXmlSerializer)
|
||||
public ExportDictionaryController(IDictionaryItemService dictionaryItemService, IEntityXmlSerializer entityXmlSerializer)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
_dictionaryItemService = dictionaryItemService;
|
||||
_entityXmlSerializer = entityXmlSerializer;
|
||||
}
|
||||
|
||||
[HttpGet("export/{key:guid}")]
|
||||
[HttpGet("{key:guid}/export")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ExportDictionary(Guid key, bool includeChildren = false)
|
||||
public async Task<IActionResult> Export(Guid key, bool includeChildren = false)
|
||||
{
|
||||
IDictionaryItem? dictionaryItem = _localizationService.GetDictionaryItemById(key);
|
||||
IDictionaryItem? dictionaryItem = await _dictionaryItemService.GetAsync(key);
|
||||
if (dictionaryItem is null)
|
||||
{
|
||||
return await Task.FromResult(NotFound());
|
||||
@@ -35,11 +34,6 @@ public class ExportDictionaryController : DictionaryControllerBase
|
||||
|
||||
XElement xml = _entityXmlSerializer.Serialize(dictionaryItem, includeChildren);
|
||||
|
||||
var fileName = $"{dictionaryItem.ItemKey}.udt";
|
||||
|
||||
// Set custom header so umbRequestHelper.downloadFile can save the correct filename
|
||||
HttpContext.Response.Headers.Add("x-filename", fileName);
|
||||
|
||||
return await Task.FromResult(File(Encoding.UTF8.GetBytes(xml.ToDataString()), MediaTypeNames.Application.Octet, fileName));
|
||||
return await Task.FromResult(File(Encoding.UTF8.GetBytes(xml.ToDataString()), MediaTypeNames.Application.Octet, $"{dictionaryItem.ItemKey}.udt"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,53 @@
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Extensions;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Management.Services;
|
||||
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
|
||||
using Umbraco.Cms.Api.Management.Services.OperationStatus;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
public class ImportDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
private readonly IDictionaryService _dictionaryService;
|
||||
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||
private readonly ILoadDictionaryItemService _loadDictionaryItemService;
|
||||
private readonly IDictionaryItemImportService _dictionaryItemImportService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public ImportDictionaryController(
|
||||
IDictionaryService dictionaryService,
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
ILoadDictionaryItemService loadDictionaryItemService)
|
||||
IDictionaryItemImportService dictionaryItemImportService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_dictionaryService = dictionaryService;
|
||||
_webHostEnvironment = webHostEnvironment;
|
||||
_loadDictionaryItemService = loadDictionaryItemService;
|
||||
_dictionaryItemImportService = dictionaryItemImportService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpPost("import")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(ContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ImportDictionary(string file, int? parentId)
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Import(ViewModels.Dictionary.DictionaryImportModel dictionaryImportModel)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(file))
|
||||
Attempt<IDictionaryItem?, DictionaryImportOperationStatus> result = await _dictionaryItemImportService
|
||||
.ImportDictionaryItemFromUdtFileAsync(
|
||||
dictionaryImportModel.FileName,
|
||||
dictionaryImportModel.ParentKey,
|
||||
CurrentUserId(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Status switch
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(_webHostEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file);
|
||||
if (_webHostEnvironment.ContentRootFileProvider.GetFileInfo(filePath) is null)
|
||||
{
|
||||
return await Task.FromResult(NotFound());
|
||||
}
|
||||
|
||||
IDictionaryItem dictionaryItem = _loadDictionaryItemService.Load(filePath, parentId);
|
||||
|
||||
return await Task.FromResult(Content(_dictionaryService.CalculatePath(dictionaryItem.ParentId, dictionaryItem.Id), MediaTypeNames.Text.Plain, Encoding.UTF8));
|
||||
DictionaryImportOperationStatus.Success => CreatedAtAction<ByKeyDictionaryController>(controller => nameof(controller.ByKey), result.Result!.Key),
|
||||
DictionaryImportOperationStatus.ParentNotFound => NotFound("The parent dictionary item could not be found."),
|
||||
DictionaryImportOperationStatus.InvalidFileType => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Invalid file type")
|
||||
.WithDetail("The dictionary import only supports UDT files.")
|
||||
.Build()),
|
||||
DictionaryImportOperationStatus.InvalidFileContent => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Invalid file content")
|
||||
.WithDetail("The uploaded file could not be read as a valid UDT file.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown dictionary import operation status")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,46 @@
|
||||
using System.Xml;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Management.Models;
|
||||
using Umbraco.Cms.Api.Management.Services;
|
||||
using Umbraco.Cms.Api.Management.Services.OperationStatus;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.New.Cms.Core.Factories;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
public class UploadDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
private readonly IUploadFileService _uploadFileService;
|
||||
private readonly IDictionaryFactory _dictionaryFactory;
|
||||
|
||||
public UploadDictionaryController(ILocalizedTextService localizedTextService, IUploadFileService uploadFileService, IDictionaryFactory dictionaryFactory)
|
||||
public UploadDictionaryController(IUploadFileService uploadFileService, IDictionaryFactory dictionaryFactory)
|
||||
{
|
||||
_localizedTextService = localizedTextService;
|
||||
_uploadFileService = uploadFileService;
|
||||
_dictionaryFactory = dictionaryFactory;
|
||||
}
|
||||
|
||||
[HttpPost("upload")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(DictionaryImportViewModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(DictionaryUploadViewModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<DictionaryImportViewModel>> Upload(IFormFile file)
|
||||
public async Task<IActionResult> Upload(IFormFile file)
|
||||
{
|
||||
FormFileUploadResult formFileUploadResult = _uploadFileService.TryLoad(file);
|
||||
if (formFileUploadResult.CouldLoad is false || formFileUploadResult.XmlDocument is null)
|
||||
Attempt<UdtFileUpload, UdtFileUploadOperationStatus> result = await _uploadFileService.UploadUdtFileAsync(file);
|
||||
|
||||
return result.Status switch
|
||||
{
|
||||
return await Task.FromResult(ValidationProblem(
|
||||
_localizedTextService.Localize("media", "failedFileUpload"),
|
||||
formFileUploadResult.ErrorMessage));
|
||||
}
|
||||
|
||||
DictionaryImportViewModel model = _dictionaryFactory.CreateDictionaryImportViewModel(formFileUploadResult);
|
||||
|
||||
if (!model.DictionaryItems.Any())
|
||||
{
|
||||
return ValidationProblem(
|
||||
_localizedTextService.Localize("media", "failedFileUpload"),
|
||||
_localizedTextService.Localize("dictionary", "noItemsInFile"));
|
||||
}
|
||||
|
||||
return await Task.FromResult(model);
|
||||
UdtFileUploadOperationStatus.Success => Ok(_dictionaryFactory.CreateDictionaryImportViewModel(result.Result)),
|
||||
UdtFileUploadOperationStatus.InvalidFileType => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Invalid file type")
|
||||
.WithDetail("The dictionary import only supports UDT files.")
|
||||
.Build()),
|
||||
UdtFileUploadOperationStatus.InvalidFileContent => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Invalid file content")
|
||||
.WithDetail("The uploaded file could not be read as a valid UDT file.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown UDT file upload operation status")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ internal static class DictionaryBuilderExtensions
|
||||
{
|
||||
builder.Services
|
||||
.AddTransient<IDictionaryFactory, DictionaryFactory>()
|
||||
.AddTransient<ILoadDictionaryItemService, LoadDictionaryItemService>();
|
||||
.AddTransient<IDictionaryItemImportService, DictionaryItemImportService>();
|
||||
|
||||
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<DictionaryMapDefinition>();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ public static class FileUploadBuilderExtensions
|
||||
internal static IUmbracoBuilder AddFileUpload(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddTransient<IUploadFileService, UploadFileService>();
|
||||
builder.Services.AddTransient<ITemporaryFileService, TemporaryFileService>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Xml;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Management.Models;
|
||||
@@ -55,37 +54,36 @@ public class DictionaryFactory : IDictionaryFactory
|
||||
return updated;
|
||||
}
|
||||
|
||||
public DictionaryImportViewModel CreateDictionaryImportViewModel(FormFileUploadResult formFileUploadResult)
|
||||
{
|
||||
if (formFileUploadResult.CouldLoad is false || formFileUploadResult.XmlDocument is null)
|
||||
public DictionaryUploadViewModel CreateDictionaryImportViewModel(UdtFileUpload udtFileUpload) =>
|
||||
new DictionaryUploadViewModel
|
||||
{
|
||||
throw new ArgumentNullException("The document of the FormFileUploadResult cannot be null");
|
||||
}
|
||||
FileName = udtFileUpload.FileName,
|
||||
DictionaryItems = udtFileUpload
|
||||
.Content
|
||||
.Descendants("DictionaryItem")
|
||||
.Select(dictionaryItem =>
|
||||
{
|
||||
if (Guid.TryParse(dictionaryItem.Attributes("Key").FirstOrDefault()?.Value, out Guid itemKey) == false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var model = new DictionaryImportViewModel
|
||||
{
|
||||
TempFileName = formFileUploadResult.TemporaryPath, DictionaryItems = new List<DictionaryItemsImportViewModel>(),
|
||||
var name = dictionaryItem.Attributes("Name").FirstOrDefault()?.Value;
|
||||
if (name.IsNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Guid? parentKey = Guid.TryParse(dictionaryItem.Parent?.Attributes("Key").FirstOrDefault()?.Value, out Guid key)
|
||||
? key
|
||||
: null;
|
||||
|
||||
return new DictionaryItemsImportViewModel { Name = name, Key = itemKey, ParentKey = parentKey };
|
||||
})
|
||||
.WhereNotNull()
|
||||
.ToArray(),
|
||||
};
|
||||
|
||||
var level = 1;
|
||||
var currentParent = string.Empty;
|
||||
foreach (XmlNode dictionaryItem in formFileUploadResult.XmlDocument.GetElementsByTagName("DictionaryItem"))
|
||||
{
|
||||
var name = dictionaryItem.Attributes?.GetNamedItem("Name")?.Value ?? string.Empty;
|
||||
var parentKey = dictionaryItem.ParentNode?.Attributes?.GetNamedItem("Key")?.Value ?? string.Empty;
|
||||
|
||||
if (parentKey != currentParent || level == 1)
|
||||
{
|
||||
level += 1;
|
||||
currentParent = parentKey;
|
||||
}
|
||||
|
||||
model.DictionaryItems.Add(new DictionaryItemsImportViewModel { Level = level, Name = name });
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
private async Task MapTranslations(IDictionaryItem dictionaryItem, IEnumerable<DictionaryItemTranslationModel> translationModels)
|
||||
{
|
||||
var languagesByIsoCode = (await _languageService.GetAllAsync()).ToDictionary(l => l.IsoCode);
|
||||
|
||||
@@ -12,5 +12,5 @@ public interface IDictionaryFactory
|
||||
|
||||
Task<DictionaryItemViewModel> CreateDictionaryItemViewModelAsync(IDictionaryItem dictionaryItem);
|
||||
|
||||
DictionaryImportViewModel CreateDictionaryImportViewModel(FormFileUploadResult formFileUploadResult);
|
||||
DictionaryUploadViewModel CreateDictionaryImportViewModel(UdtFileUpload fileUpload);
|
||||
}
|
||||
|
||||
@@ -45,11 +45,12 @@ public class DictionaryMapDefinition : IMapDefinition
|
||||
target.DeleteDate = null;
|
||||
}
|
||||
|
||||
// Umbraco.Code.MapAll -Level
|
||||
// Umbraco.Code.MapAll
|
||||
private void Map(IDictionaryItem source, DictionaryOverviewViewModel target, MapperContext context)
|
||||
{
|
||||
target.Key = source.Key;
|
||||
target.Name = source.ItemKey;
|
||||
target.ParentKey = source.ParentId;
|
||||
target.TranslatedIsoCodes = source
|
||||
.Translations
|
||||
.Where(translation => translation.Value.IsNullOrWhiteSpace() == false)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
using System.Xml;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Models;
|
||||
|
||||
public class FormFileUploadResult
|
||||
{
|
||||
public bool CouldLoad { get; set; }
|
||||
|
||||
public XmlDocument? XmlDocument { get; set; }
|
||||
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
public string? TemporaryPath { get; set; }
|
||||
}
|
||||
10
src/Umbraco.Cms.Api.Management/Models/UdtFileUpload.cs
Normal file
10
src/Umbraco.Cms.Api.Management/Models/UdtFileUpload.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Models;
|
||||
|
||||
public class UdtFileUpload
|
||||
{
|
||||
public required XDocument Content { get; set; }
|
||||
|
||||
public required string FileName { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -766,12 +766,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/dictionary/export/{key}": {
|
||||
"/umbraco/management/api/v1/dictionary/{key}/export": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Dictionary"
|
||||
],
|
||||
"operationId": "GetDictionaryExportByKey",
|
||||
"operationId": "GetDictionaryByKeyExport",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "key",
|
||||
@@ -822,30 +822,25 @@
|
||||
"Dictionary"
|
||||
],
|
||||
"operationId": "PostDictionaryImport",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "file",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "parentId",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DictionaryImportModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"201": {
|
||||
"description": "Created"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentResultModel"
|
||||
"$ref": "#/components/schemas/ProblemDetailsModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -855,7 +850,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotFoundResultModel"
|
||||
"$ref": "#/components/schemas/ProblemDetailsModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -878,7 +873,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DictionaryImportModel"
|
||||
"$ref": "#/components/schemas/DictionaryUploadModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5203,25 +5198,6 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ContentResultModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"contentType": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"statusCode": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ContentTreeItemModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -5568,14 +5544,12 @@
|
||||
"DictionaryImportModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dictionaryItems": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/DictionaryItemsImportModel"
|
||||
}
|
||||
"fileName": {
|
||||
"type": "string"
|
||||
},
|
||||
"tempFileName": {
|
||||
"parentKey": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
@@ -5650,13 +5624,18 @@
|
||||
"DictionaryItemsImportModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"level": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
"parentKey": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -5672,9 +5651,10 @@
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"level": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
"parentKey": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"nullable": true
|
||||
},
|
||||
"translatedIsoCodes": {
|
||||
"type": "array",
|
||||
@@ -5685,6 +5665,22 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"DictionaryUploadModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dictionaryItems": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/DictionaryItemsImportModel"
|
||||
}
|
||||
},
|
||||
"fileName": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"DirectionModel": {
|
||||
"enum": [
|
||||
"Ascending",
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Api.Management.Services.OperationStatus;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Packaging;
|
||||
using Umbraco.Extensions;
|
||||
using File = System.IO.File;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Services;
|
||||
|
||||
internal sealed class DictionaryItemImportService : IDictionaryItemImportService
|
||||
{
|
||||
private readonly IDictionaryItemService _dictionaryItemService;
|
||||
private readonly PackageDataInstallation _packageDataInstallation;
|
||||
private readonly ILogger<DictionaryItemImportService> _logger;
|
||||
private readonly ITemporaryFileService _temporaryFileService;
|
||||
|
||||
public DictionaryItemImportService(
|
||||
IDictionaryItemService dictionaryItemService,
|
||||
PackageDataInstallation packageDataInstallation,
|
||||
ILogger<DictionaryItemImportService> logger,
|
||||
ITemporaryFileService temporaryFileService)
|
||||
{
|
||||
_dictionaryItemService = dictionaryItemService;
|
||||
_packageDataInstallation = packageDataInstallation;
|
||||
_logger = logger;
|
||||
_temporaryFileService = temporaryFileService;
|
||||
}
|
||||
|
||||
public async Task<Attempt<IDictionaryItem?, DictionaryImportOperationStatus>> ImportDictionaryItemFromUdtFileAsync(string fileName, Guid? parentKey, int userId = Constants.Security.SuperUserId)
|
||||
{
|
||||
if (".udt".InvariantEquals(Path.GetExtension(fileName)) == false)
|
||||
{
|
||||
return Attempt.FailWithStatus<IDictionaryItem?, DictionaryImportOperationStatus>(DictionaryImportOperationStatus.InvalidFileType, null);
|
||||
}
|
||||
|
||||
if (parentKey.HasValue && await _dictionaryItemService.GetAsync(parentKey.Value) == null)
|
||||
{
|
||||
return Attempt.FailWithStatus<IDictionaryItem?, DictionaryImportOperationStatus>(DictionaryImportOperationStatus.ParentNotFound, null);
|
||||
}
|
||||
|
||||
// load the UDT file from disk
|
||||
(XDocument Document, DictionaryImportOperationStatus Status) loadResult = await LoadUdtFileAsync(fileName);
|
||||
if (loadResult.Status != DictionaryImportOperationStatus.Success)
|
||||
{
|
||||
return Attempt.FailWithStatus<IDictionaryItem?, DictionaryImportOperationStatus>(loadResult.Status, null);
|
||||
}
|
||||
|
||||
// import the UDT file
|
||||
(IDictionaryItem? DictionaryItem, DictionaryImportOperationStatus Status) importResult = ImportUdtFile(loadResult.Document, userId, parentKey, fileName);
|
||||
|
||||
// clean up the UDT file (we don't care about success or failure at this point, we'll let the temporary file service handle those)
|
||||
await _temporaryFileService.DeleteFileAsync(fileName);
|
||||
|
||||
return importResult.Status == DictionaryImportOperationStatus.Success
|
||||
? Attempt.SucceedWithStatus(DictionaryImportOperationStatus.Success, importResult.DictionaryItem)
|
||||
: Attempt.FailWithStatus<IDictionaryItem?, DictionaryImportOperationStatus>(importResult.Status, null);
|
||||
}
|
||||
|
||||
private async Task<(XDocument Document, DictionaryImportOperationStatus Status)> LoadUdtFileAsync(string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = await _temporaryFileService.GetFilePathAsync(fileName);
|
||||
|
||||
await using FileStream stream = File.OpenRead(filePath);
|
||||
XDocument document = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None);
|
||||
|
||||
return document.Root != null
|
||||
? (document, DictionaryImportOperationStatus.Success)
|
||||
: (document, DictionaryImportOperationStatus.InvalidFileContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading UDT file: {FileName}", fileName);
|
||||
return (new XDocument(), DictionaryImportOperationStatus.InvalidFileContent);
|
||||
}
|
||||
}
|
||||
|
||||
private (IDictionaryItem? DictionaryItem, DictionaryImportOperationStatus Status) ImportUdtFile(XDocument udtFileContent, int userId, Guid? parentKey, string fileName)
|
||||
{
|
||||
if (udtFileContent.Root == null)
|
||||
{
|
||||
return (null, DictionaryImportOperationStatus.InvalidFileContent);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IEnumerable<IDictionaryItem> dictionaryItems = _packageDataInstallation.ImportDictionaryItem(udtFileContent.Root, userId, parentKey);
|
||||
IDictionaryItem? importedDictionaryItem = dictionaryItems.FirstOrDefault();
|
||||
return importedDictionaryItem != null
|
||||
? (importedDictionaryItem, DictionaryImportOperationStatus.Success)
|
||||
: (null, DictionaryImportOperationStatus.InvalidFileContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error importing UDT file: {FileName}", fileName);
|
||||
return (null, DictionaryImportOperationStatus.InvalidFileContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Umbraco.Cms.Api.Management.Services.OperationStatus;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Services;
|
||||
|
||||
public interface IDictionaryItemImportService
|
||||
{
|
||||
Task<Attempt<IDictionaryItem?, DictionaryImportOperationStatus>> ImportDictionaryItemFromUdtFileAsync(string fileName, Guid? parentKey, int userId = Constants.Security.SuperUserId);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Services;
|
||||
|
||||
public interface ILoadDictionaryItemService
|
||||
{
|
||||
IDictionaryItem Load(string filePath, int? parentId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Services;
|
||||
|
||||
public interface ITemporaryFileService
|
||||
{
|
||||
Task<string> GetFilePathAsync(string fileName);
|
||||
|
||||
Task<string> SaveFileAsync(IFormFile file);
|
||||
|
||||
Task<bool> DeleteFileAsync(string fileName);
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Api.Management.Models;
|
||||
using Umbraco.Cms.Api.Management.Services.OperationStatus;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Services;
|
||||
|
||||
public interface IUploadFileService
|
||||
{
|
||||
FormFileUploadResult TryLoad(IFormFile file);
|
||||
Task<Attempt<UdtFileUpload, UdtFileUploadOperationStatus>> UploadUdtFileAsync(IFormFile file);
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Packaging;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Services;
|
||||
|
||||
public class LoadDictionaryItemService : ILoadDictionaryItemService
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
// FIXME: use IDictionaryItemService instead of ILocalizationService
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly PackageDataInstallation _packageDataInstallation;
|
||||
private readonly ILogger<LoadDictionaryItemService> _logger;
|
||||
|
||||
public LoadDictionaryItemService(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
ILocalizationService localizationService,
|
||||
PackageDataInstallation packageDataInstallation,
|
||||
ILogger<LoadDictionaryItemService> logger)
|
||||
{
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_localizationService = localizationService;
|
||||
_packageDataInstallation = packageDataInstallation;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// FIXME: use Guid key, not integer ID for parent identification
|
||||
public IDictionaryItem Load(string filePath, int? parentId)
|
||||
{
|
||||
var xmlDocument = new XmlDocument { XmlResolver = null };
|
||||
xmlDocument.Load(filePath);
|
||||
|
||||
var userId = _backOfficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? 0;
|
||||
var element = XElement.Parse(xmlDocument.InnerXml);
|
||||
|
||||
IDictionaryItem? parentDictionaryItem = _localizationService.GetDictionaryItemById(parentId ?? 0);
|
||||
IEnumerable<IDictionaryItem> dictionaryItems = _packageDataInstallation.ImportDictionaryItem(element, userId, parentDictionaryItem?.Key);
|
||||
|
||||
// Try to clean up the temporary file.
|
||||
try
|
||||
{
|
||||
System.IO.File.Delete(filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error cleaning up temporary udt file in {File}", filePath);
|
||||
}
|
||||
|
||||
return dictionaryItems.First();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Umbraco.Cms.Api.Management.Services.OperationStatus;
|
||||
|
||||
public enum DictionaryImportOperationStatus
|
||||
{
|
||||
Success,
|
||||
ParentNotFound,
|
||||
InvalidFileContent,
|
||||
InvalidFileType
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Umbraco.Cms.Api.Management.Services.OperationStatus;
|
||||
|
||||
public enum UdtFileUploadOperationStatus
|
||||
{
|
||||
Success,
|
||||
InvalidFileType,
|
||||
InvalidFileContent
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Services;
|
||||
|
||||
internal sealed class TemporaryFileService : ITemporaryFileService
|
||||
{
|
||||
private readonly IHostEnvironment _hostEnvironment;
|
||||
private readonly ILogger<TemporaryFileService> _logger;
|
||||
|
||||
public TemporaryFileService(IHostEnvironment hostEnvironment, ILogger<TemporaryFileService> logger)
|
||||
{
|
||||
_hostEnvironment = hostEnvironment;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<string> GetFilePathAsync(string fileName)
|
||||
{
|
||||
var root = _hostEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads);
|
||||
fileName = fileName.Trim(Constants.CharArrays.DoubleQuote);
|
||||
|
||||
var filePath = Path.Combine(root, fileName);
|
||||
return await Task.FromResult(filePath);
|
||||
}
|
||||
|
||||
public async Task<string> SaveFileAsync(IFormFile file)
|
||||
{
|
||||
var filePath = await GetFilePathAsync(file.FileName);
|
||||
|
||||
await using FileStream fileStream = File.Create(filePath);
|
||||
await file.CopyToAsync(fileStream);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteFileAsync(string fileName)
|
||||
{
|
||||
var filePath = await GetFilePathAsync(fileName);
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting temporary file: {FilePath}", filePath);
|
||||
return await Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +1,42 @@
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Extensions;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Management.Models;
|
||||
using Umbraco.Cms.Api.Management.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Services;
|
||||
|
||||
public class UploadFileService : IUploadFileService
|
||||
internal sealed class UploadFileService : IUploadFileService
|
||||
{
|
||||
private readonly IHostEnvironment _hostEnvironment;
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
private readonly ITemporaryFileService _temporaryFileService;
|
||||
|
||||
public UploadFileService(IHostEnvironment hostEnvironment, ILocalizedTextService localizedTextService)
|
||||
public UploadFileService(ITemporaryFileService temporaryFileService) => _temporaryFileService = temporaryFileService;
|
||||
|
||||
public async Task<Attempt<UdtFileUpload, UdtFileUploadOperationStatus>> UploadUdtFileAsync(IFormFile file)
|
||||
{
|
||||
_hostEnvironment = hostEnvironment;
|
||||
_localizedTextService = localizedTextService;
|
||||
}
|
||||
UdtFileUpload DefaultModel() => new() { FileName = file.FileName, Content = new XDocument() };
|
||||
|
||||
public FormFileUploadResult TryLoad(IFormFile file)
|
||||
{
|
||||
var formFileUploadResult = new FormFileUploadResult();
|
||||
var fileName = file.FileName.Trim(Constants.CharArrays.DoubleQuote);
|
||||
var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower();
|
||||
var root = _hostEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads);
|
||||
formFileUploadResult.TemporaryPath = Path.Combine(root, fileName);
|
||||
|
||||
if (!Path.GetFullPath(formFileUploadResult.TemporaryPath).StartsWith(Path.GetFullPath(root)))
|
||||
if (".udt".InvariantEquals(Path.GetExtension(file.FileName)) == false)
|
||||
{
|
||||
formFileUploadResult.ErrorMessage = _localizedTextService.Localize("media", "invalidFileName");
|
||||
formFileUploadResult.CouldLoad = false;
|
||||
return formFileUploadResult;
|
||||
return Attempt.FailWithStatus(UdtFileUploadOperationStatus.InvalidFileType, DefaultModel());
|
||||
}
|
||||
|
||||
if (!ext.InvariantEquals("udt"))
|
||||
var filePath = await _temporaryFileService.SaveFileAsync(file);
|
||||
|
||||
XDocument content;
|
||||
await using (FileStream fileStream = File.OpenRead(filePath))
|
||||
{
|
||||
formFileUploadResult.ErrorMessage = _localizedTextService.Localize("media", "disallowedFileType");
|
||||
formFileUploadResult.CouldLoad = false;
|
||||
return formFileUploadResult;
|
||||
content = await XDocument.LoadAsync(fileStream, LoadOptions.None, CancellationToken.None);
|
||||
}
|
||||
|
||||
using (FileStream stream = File.Create(formFileUploadResult.TemporaryPath))
|
||||
if (content.Root == null)
|
||||
{
|
||||
file.CopyToAsync(stream).GetAwaiter().GetResult();
|
||||
return Attempt.FailWithStatus(UdtFileUploadOperationStatus.InvalidFileContent, DefaultModel());
|
||||
}
|
||||
|
||||
formFileUploadResult.XmlDocument = new XmlDocument {XmlResolver = null};
|
||||
formFileUploadResult.XmlDocument.Load(formFileUploadResult.TemporaryPath);
|
||||
|
||||
if (formFileUploadResult.XmlDocument.DocumentElement != null)
|
||||
{
|
||||
return formFileUploadResult;
|
||||
}
|
||||
|
||||
formFileUploadResult.ErrorMessage = _localizedTextService.Localize("speechBubbles", "fileErrorNotFound");
|
||||
formFileUploadResult.CouldLoad = false;
|
||||
return formFileUploadResult;
|
||||
|
||||
// grab the file name from the file path in case the temporary file name is different from the uploaded one
|
||||
var model = new UdtFileUpload { FileName = Path.GetFileName(filePath), Content = content };
|
||||
return Attempt.SucceedWithStatus(UdtFileUploadOperationStatus.Success, model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.Dictionary;
|
||||
|
||||
public class DictionaryImportModel
|
||||
{
|
||||
public required string FileName { get; set; }
|
||||
|
||||
public Guid? ParentKey { get; set; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.Dictionary;
|
||||
|
||||
public class DictionaryImportViewModel
|
||||
{
|
||||
public List<DictionaryItemsImportViewModel> DictionaryItems { get; set; } = null!;
|
||||
|
||||
public string? TempFileName { get; set; }
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
public class DictionaryItemsImportViewModel
|
||||
{
|
||||
public Guid Key { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public int Level { get; set; }
|
||||
public Guid? ParentKey { get; set; }
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ public class DictionaryOverviewViewModel
|
||||
public Guid Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the level.
|
||||
/// Gets or sets the parent key.
|
||||
/// </summary>
|
||||
public int Level { get; set; }
|
||||
public Guid? ParentKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the translations.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.Dictionary;
|
||||
|
||||
public class DictionaryUploadViewModel
|
||||
{
|
||||
public IEnumerable<DictionaryItemsImportViewModel> DictionaryItems { get; set; } = Array.Empty<DictionaryItemsImportViewModel>();
|
||||
|
||||
public string? FileName { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user