Temporary files endpoints for management api (#13944)
* Added temporary file uploads including a repository implementation using local temp folder. * missing files * Fixed copy paste error * Updated OpenApi.json * Updated OpenApi.json * Added file extension check * Clean up. Removed old TemporaryFileService and UploadFileService and updated dictionary items to use this new items * Clean up * Get rid of stream directly on TemporaryFileModel, and use delegate to open stream instead. * Review changes * Moved models to their own files * Reverted launch settings * Update open API JSON file --------- Co-authored-by: kjac <kja@umbraco.dk>
This commit is contained in:
@@ -32,7 +32,7 @@ public class ImportDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
Attempt<IDictionaryItem?, DictionaryImportOperationStatus> result = await _dictionaryItemImportService
|
||||
.ImportDictionaryItemFromUdtFileAsync(
|
||||
importDictionaryRequestModel.FileName,
|
||||
importDictionaryRequestModel.TemporaryFileKey,
|
||||
importDictionaryRequestModel.ParentKey,
|
||||
CurrentUserId(_backOfficeSecurityAccessor));
|
||||
|
||||
@@ -40,6 +40,7 @@ public class ImportDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
DictionaryImportOperationStatus.Success => CreatedAtAction<ByKeyDictionaryController>(controller => nameof(controller.ByKey), result.Result!.Key),
|
||||
DictionaryImportOperationStatus.ParentNotFound => NotFound("The parent dictionary item could not be found."),
|
||||
DictionaryImportOperationStatus.TemporaryFileNotFound => NotFound("The temporary file with specified key could not be found."),
|
||||
DictionaryImportOperationStatus.InvalidFileType => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Invalid file type")
|
||||
.WithDetail("The dictionary import only supports UDT files.")
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
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.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
public class UploadDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
private readonly IUploadFileService _uploadFileService;
|
||||
private readonly IDictionaryPresentationFactory _dictionaryPresentationFactory;
|
||||
|
||||
public UploadDictionaryController(IUploadFileService uploadFileService, IDictionaryPresentationFactory dictionaryPresentationFactory)
|
||||
{
|
||||
_uploadFileService = uploadFileService;
|
||||
_dictionaryPresentationFactory = dictionaryPresentationFactory;
|
||||
}
|
||||
|
||||
[HttpPost("upload")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(UploadDictionaryResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> Upload(IFormFile file)
|
||||
{
|
||||
Attempt<UdtFileUpload, UdtFileUploadOperationStatus> result = await _uploadFileService.UploadUdtFileAsync(file);
|
||||
|
||||
return result.Status switch
|
||||
{
|
||||
UdtFileUploadOperationStatus.Success => Ok(_dictionaryPresentationFactory.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")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Language;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.TemporaryFile;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.TemporaryFile;
|
||||
|
||||
public class ByKeyTemporaryFileController : TemporaryFileControllerBase
|
||||
{
|
||||
private readonly ITemporaryFileService _temporaryFileService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
|
||||
public ByKeyTemporaryFileController(ITemporaryFileService temporaryFileService, IUmbracoMapper umbracoMapper)
|
||||
{
|
||||
_temporaryFileService = temporaryFileService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[HttpGet($"{{{nameof(key)}}}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(TemporaryFileResponseModel), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> ByKey(Guid key)
|
||||
{
|
||||
TemporaryFileModel? model = await _temporaryFileService.GetAsync(key);
|
||||
if (model == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(_umbracoMapper.Map<TemporaryFileModel, TemporaryFileResponseModel>(model));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.TemporaryFile;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.TemporaryFile;
|
||||
|
||||
public class CreateTemporaryFileController : TemporaryFileControllerBase
|
||||
{
|
||||
private readonly ITemporaryFileService _temporaryFileService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
|
||||
public CreateTemporaryFileController(ITemporaryFileService temporaryFileService, IUmbracoMapper umbracoMapper)
|
||||
{
|
||||
_temporaryFileService = temporaryFileService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> Create([FromForm] CreateTemporaryFileRequestModel model)
|
||||
{
|
||||
CreateTemporaryFileModel createModel = _umbracoMapper.Map<CreateTemporaryFileRequestModel, CreateTemporaryFileModel>(model)!;
|
||||
|
||||
Attempt<TemporaryFileModel?, TemporaryFileOperationStatus> result = await _temporaryFileService.CreateAsync(createModel);
|
||||
|
||||
return result.Success
|
||||
? CreatedAtAction<ByKeyTemporaryFileController>(controller => nameof(controller.ByKey), new { key = result.Result!.Key })
|
||||
: TemporaryFileStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.TemporaryFile;
|
||||
|
||||
public class DeleteTemporaryFileController : TemporaryFileControllerBase
|
||||
{
|
||||
private readonly ITemporaryFileService _temporaryFileService;
|
||||
|
||||
public DeleteTemporaryFileController(ITemporaryFileService temporaryFileService) => _temporaryFileService = temporaryFileService;
|
||||
|
||||
[HttpDelete($"{{{nameof(key)}}}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Delete(Guid key)
|
||||
{
|
||||
Attempt<TemporaryFileModel?, TemporaryFileOperationStatus> result = await _temporaryFileService.DeleteAsync(key);
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
: TemporaryFileStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.TemporaryFile;
|
||||
|
||||
[ApiController]
|
||||
[VersionedApiBackOfficeRoute("temporaryfile")]
|
||||
[ApiExplorerSettings(GroupName = "Temporary File")]
|
||||
[ApiVersion("1.0")]
|
||||
public abstract class TemporaryFileControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
protected IActionResult TemporaryFileStatusResult(TemporaryFileOperationStatus operationStatus) =>
|
||||
operationStatus switch
|
||||
{
|
||||
TemporaryFileOperationStatus.FileExtensionNotAllowed => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("File extension not allowed")
|
||||
.WithDetail("The file extension is not allowed.")
|
||||
.Build()),
|
||||
|
||||
TemporaryFileOperationStatus.KeyAlreadyUsed => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Key already used")
|
||||
.WithDetail("The specified key is already used.")
|
||||
.Build()),
|
||||
|
||||
TemporaryFileOperationStatus.NotFound => NotFound(),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown temporary file operation status")
|
||||
};
|
||||
|
||||
|
||||
protected new IActionResult NotFound() => NotFound("The temporary file could not be found");
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Services;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
|
||||
public static class FileUploadBuilderExtensions
|
||||
{
|
||||
internal static IUmbracoBuilder AddFileUpload(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddTransient<IUploadFileService, UploadFileService>();
|
||||
builder.Services.AddTransient<ITemporaryFileService, TemporaryFileService>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Umbraco.Cms.Api.Management.Mapping.TemporaryFile;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
|
||||
internal static class TTemporaryFileBuilderExtensions
|
||||
{
|
||||
internal static IUmbracoBuilder AddTemporaryFiles(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>()
|
||||
.Add<TemporaryFileViewModelsMapDefinition>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -32,11 +32,11 @@ public class ManagementApiComposer : IComposer
|
||||
.AddMediaTypes()
|
||||
.AddLanguages()
|
||||
.AddDictionary()
|
||||
.AddFileUpload()
|
||||
.AddHealthChecks()
|
||||
.AddModelsBuilder()
|
||||
.AddRedirectUrl()
|
||||
.AddTrackedReferences()
|
||||
.AddTemporaryFiles()
|
||||
.AddDataTypes()
|
||||
.AddTemplates()
|
||||
.AddRelationTypes()
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.TemporaryFile;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Mapping.TemporaryFile;
|
||||
|
||||
public class TemporaryFileViewModelsMapDefinition : IMapDefinition
|
||||
{
|
||||
public void DefineMaps(IUmbracoMapper mapper)
|
||||
{
|
||||
mapper.Define<CreateTemporaryFileRequestModel, CreateTemporaryFileModel>((source, context) => new CreateTemporaryFileModel { FileName = string.Empty }, Map);
|
||||
mapper.Define<TemporaryFileModel, TemporaryFileResponseModel>((source, context) => new TemporaryFileResponseModel(), Map);
|
||||
}
|
||||
|
||||
// Umbraco.Code.MapAll
|
||||
private void Map(CreateTemporaryFileRequestModel source, CreateTemporaryFileModel target, MapperContext context)
|
||||
{
|
||||
target.OpenReadStream = () => source.File.OpenReadStream();
|
||||
target.FileName = source.File.FileName;
|
||||
target.Key = source.Key;
|
||||
}
|
||||
|
||||
// Umbraco.Code.MapAll
|
||||
private void Map(TemporaryFileModel source, TemporaryFileResponseModel target, MapperContext context)
|
||||
{
|
||||
target.Key = source.Key;
|
||||
target.AvailableUntil = source.AvailableUntil;
|
||||
target.FileName = source.FileName;
|
||||
}
|
||||
}
|
||||
@@ -1166,43 +1166,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/dictionary/upload": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Dictionary"
|
||||
],
|
||||
"operationId": "PostDictionaryUpload",
|
||||
"requestBody": {
|
||||
"content": { }
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/UploadDictionaryResponseModel"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetailsModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/tree/dictionary/children": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -6272,6 +6235,163 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/temporaryfile": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Temporary File"
|
||||
],
|
||||
"operationId": "PostTemporaryfile",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Key": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"File": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
},
|
||||
"encoding": {
|
||||
"Key": {
|
||||
"style": "form"
|
||||
},
|
||||
"File": {
|
||||
"style": "form"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"headers": {
|
||||
"Location": {
|
||||
"description": "Location of the newly created resource",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"description": "Location of the newly created resource",
|
||||
"format": "uri"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetailsModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/temporaryfile/{key}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Temporary File"
|
||||
],
|
||||
"operationId": "GetTemporaryfileByKey",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "key",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetailsModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetailsModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/TemporaryFileResponseModel"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Temporary File"
|
||||
],
|
||||
"operationId": "DeleteTemporaryfileByKey",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "key",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetailsModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetailsModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/tracked-reference/{key}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -8215,30 +8335,12 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ImportDictionaryItemsPresentationModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"parentKey": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ImportDictionaryRequestModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fileName": {
|
||||
"type": "string"
|
||||
"temporaryFileKey": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"parentKey": {
|
||||
"type": "string",
|
||||
@@ -10303,6 +10405,24 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"TemporaryFileResponseModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"availableUntil": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"fileName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"TreeItemPresentationModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -10506,26 +10626,6 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"UploadDictionaryResponseModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dictionaryItems": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ImportDictionaryItemsPresentationModel"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"fileName": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"UserGroupBaseModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -5,7 +5,7 @@ using Umbraco.Extensions;
|
||||
namespace Umbraco.Cms.Api.Management.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// This filter explicitly removes all other mime types than application/json from the produced OpenAPI document
|
||||
/// This filter explicitly removes all other mime types than application/json from the produced OpenAPI document when application/json is accepted.
|
||||
/// </summary>
|
||||
public class MimeTypeDocumentFilter : IDocumentFilter
|
||||
{
|
||||
@@ -15,8 +15,14 @@ public class MimeTypeDocumentFilter : IDocumentFilter
|
||||
.SelectMany(path => path.Value.Operations.Values)
|
||||
.ToArray();
|
||||
|
||||
void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType> content) =>
|
||||
content.RemoveAll(r => r.Key != "application/json");
|
||||
void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType> content)
|
||||
{
|
||||
if (content.ContainsKey("application/json"))
|
||||
{
|
||||
content.RemoveAll(r => r.Key != "application/json");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
OpenApiRequestBody[] requestBodies = operations.Select(operation => operation.RequestBody).WhereNotNull().ToArray();
|
||||
foreach (OpenApiRequestBody requestBody in requestBodies)
|
||||
|
||||
@@ -3,10 +3,13 @@ using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Api.Management.Services.OperationStatus;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Packaging;
|
||||
using Umbraco.Extensions;
|
||||
using File = System.IO.File;
|
||||
using IScopeProvider = Umbraco.Cms.Infrastructure.Scoping.IScopeProvider;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Services;
|
||||
|
||||
@@ -16,22 +19,37 @@ internal sealed class DictionaryItemImportService : IDictionaryItemImportService
|
||||
private readonly PackageDataInstallation _packageDataInstallation;
|
||||
private readonly ILogger<DictionaryItemImportService> _logger;
|
||||
private readonly ITemporaryFileService _temporaryFileService;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
|
||||
public DictionaryItemImportService(
|
||||
IDictionaryItemService dictionaryItemService,
|
||||
PackageDataInstallation packageDataInstallation,
|
||||
ILogger<DictionaryItemImportService> logger,
|
||||
ITemporaryFileService temporaryFileService)
|
||||
ITemporaryFileService temporaryFileService,
|
||||
IScopeProvider scopeProvider)
|
||||
{
|
||||
_dictionaryItemService = dictionaryItemService;
|
||||
_packageDataInstallation = packageDataInstallation;
|
||||
_logger = logger;
|
||||
_temporaryFileService = temporaryFileService;
|
||||
_scopeProvider = scopeProvider;
|
||||
}
|
||||
|
||||
public async Task<Attempt<IDictionaryItem?, DictionaryImportOperationStatus>> ImportDictionaryItemFromUdtFileAsync(string fileName, Guid? parentKey, int userId = Constants.Security.SuperUserId)
|
||||
public async Task<Attempt<IDictionaryItem?, DictionaryImportOperationStatus>> ImportDictionaryItemFromUdtFileAsync(Guid fileKey, Guid? parentKey, int userId = Constants.Security.SuperUserId)
|
||||
{
|
||||
if (".udt".InvariantEquals(Path.GetExtension(fileName)) == false)
|
||||
using var scope = _scopeProvider.CreateScope();
|
||||
_temporaryFileService.EnlistDeleteIfScopeCompletes(fileKey, _scopeProvider);
|
||||
|
||||
TemporaryFileModel? temporaryFile = await _temporaryFileService.GetAsync(fileKey);
|
||||
|
||||
if (temporaryFile is null)
|
||||
{
|
||||
return Attempt.FailWithStatus<IDictionaryItem?, DictionaryImportOperationStatus>(DictionaryImportOperationStatus.TemporaryFileNotFound, null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (".udt".InvariantEquals(Path.GetExtension(temporaryFile.FileName)) == false)
|
||||
{
|
||||
return Attempt.FailWithStatus<IDictionaryItem?, DictionaryImportOperationStatus>(DictionaryImportOperationStatus.InvalidFileType, null);
|
||||
}
|
||||
@@ -42,31 +60,28 @@ internal sealed class DictionaryItemImportService : IDictionaryItemImportService
|
||||
}
|
||||
|
||||
// load the UDT file from disk
|
||||
(XDocument Document, DictionaryImportOperationStatus Status) loadResult = await LoadUdtFileAsync(fileName);
|
||||
(XDocument Document, DictionaryImportOperationStatus Status) loadResult = await LoadUdtFileAsync(temporaryFile);
|
||||
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);
|
||||
(IDictionaryItem? DictionaryItem, DictionaryImportOperationStatus Status) importResult = ImportUdtFile(loadResult.Document, userId, parentKey, temporaryFile);
|
||||
|
||||
// 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);
|
||||
scope.Complete();
|
||||
|
||||
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)
|
||||
private async Task<(XDocument Document, DictionaryImportOperationStatus Status)> LoadUdtFileAsync(TemporaryFileModel temporaryFileModel)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = await _temporaryFileService.GetFilePathAsync(fileName);
|
||||
|
||||
await using FileStream stream = File.OpenRead(filePath);
|
||||
XDocument document = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None);
|
||||
await using var dataStream = temporaryFileModel.OpenReadStream();
|
||||
XDocument document = await XDocument.LoadAsync(dataStream, LoadOptions.None, CancellationToken.None);
|
||||
|
||||
return document.Root != null
|
||||
? (document, DictionaryImportOperationStatus.Success)
|
||||
@@ -74,12 +89,12 @@ internal sealed class DictionaryItemImportService : IDictionaryItemImportService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading UDT file: {FileName}", fileName);
|
||||
_logger.LogError(ex, "Error loading UDT file: {FileName}", temporaryFileModel.FileName);
|
||||
return (new XDocument(), DictionaryImportOperationStatus.InvalidFileContent);
|
||||
}
|
||||
}
|
||||
|
||||
private (IDictionaryItem? DictionaryItem, DictionaryImportOperationStatus Status) ImportUdtFile(XDocument udtFileContent, int userId, Guid? parentKey, string fileName)
|
||||
private (IDictionaryItem? DictionaryItem, DictionaryImportOperationStatus Status) ImportUdtFile(XDocument udtFileContent, int userId, Guid? parentKey, TemporaryFileModel temporaryFileModel)
|
||||
{
|
||||
if (udtFileContent.Root == null)
|
||||
{
|
||||
@@ -96,7 +111,7 @@ internal sealed class DictionaryItemImportService : IDictionaryItemImportService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error importing UDT file: {FileName}", fileName);
|
||||
_logger.LogError(ex, "Error importing UDT file: {FileName}", temporaryFileModel.FileName);
|
||||
return (null, DictionaryImportOperationStatus.InvalidFileContent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ namespace Umbraco.Cms.Api.Management.Services;
|
||||
|
||||
public interface IDictionaryItemImportService
|
||||
{
|
||||
Task<Attempt<IDictionaryItem?, DictionaryImportOperationStatus>> ImportDictionaryItemFromUdtFileAsync(string fileName, Guid? parentKey, int userId = Constants.Security.SuperUserId);
|
||||
Task<Attempt<IDictionaryItem?, DictionaryImportOperationStatus>> ImportDictionaryItemFromUdtFileAsync(Guid temporaryFileKey, Guid? parentKey, int userId = Constants.Security.SuperUserId);
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
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,11 +0,0 @@
|
||||
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
|
||||
{
|
||||
Task<Attempt<UdtFileUpload, UdtFileUploadOperationStatus>> UploadUdtFileAsync(IFormFile file);
|
||||
}
|
||||
@@ -5,5 +5,6 @@ public enum DictionaryImportOperationStatus
|
||||
Success,
|
||||
ParentNotFound,
|
||||
InvalidFileContent,
|
||||
InvalidFileType
|
||||
InvalidFileType,
|
||||
TemporaryFileNotFound,
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
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,42 +0,0 @@
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Api.Management.Models;
|
||||
using Umbraco.Cms.Api.Management.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Services;
|
||||
|
||||
internal sealed class UploadFileService : IUploadFileService
|
||||
{
|
||||
private readonly ITemporaryFileService _temporaryFileService;
|
||||
|
||||
public UploadFileService(ITemporaryFileService temporaryFileService) => _temporaryFileService = temporaryFileService;
|
||||
|
||||
public async Task<Attempt<UdtFileUpload, UdtFileUploadOperationStatus>> UploadUdtFileAsync(IFormFile file)
|
||||
{
|
||||
UdtFileUpload DefaultModel() => new() { FileName = file.FileName, Content = new XDocument() };
|
||||
|
||||
if (".udt".InvariantEquals(Path.GetExtension(file.FileName)) == false)
|
||||
{
|
||||
return Attempt.FailWithStatus(UdtFileUploadOperationStatus.InvalidFileType, DefaultModel());
|
||||
}
|
||||
|
||||
var filePath = await _temporaryFileService.SaveFileAsync(file);
|
||||
|
||||
XDocument content;
|
||||
await using (FileStream fileStream = File.OpenRead(filePath))
|
||||
{
|
||||
content = await XDocument.LoadAsync(fileStream, LoadOptions.None, CancellationToken.None);
|
||||
}
|
||||
|
||||
if (content.Root == null)
|
||||
{
|
||||
return Attempt.FailWithStatus(UdtFileUploadOperationStatus.InvalidFileContent, DefaultModel());
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
public class ImportDictionaryRequestModel
|
||||
{
|
||||
public required string FileName { get; set; }
|
||||
public required Guid TemporaryFileKey { get; set; }
|
||||
|
||||
public Guid? ParentKey { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.TemporaryFile;
|
||||
|
||||
public class CreateTemporaryFileRequestModel
|
||||
{
|
||||
public required Guid Key { get; set; }
|
||||
|
||||
public required IFormFile File { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.TemporaryFile;
|
||||
|
||||
public class TemporaryFileResponseModel
|
||||
{
|
||||
public Guid Key { get; set; }
|
||||
|
||||
public DateTime? AvailableUntil { get; set; }
|
||||
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
[UmbracoOptions(Constants.Configuration.ConfigRuntime)]
|
||||
public class RuntimeSettings
|
||||
{
|
||||
private const string StaticTemporaryFileLifeTime = "1.00:00:00"; // TimeSpan.FromDays(1);
|
||||
/// <summary>
|
||||
/// Gets or sets the runtime mode.
|
||||
/// </summary>
|
||||
@@ -26,4 +27,10 @@ public class RuntimeSettings
|
||||
/// Gets or sets a value for the maximum request length in kb.
|
||||
/// </summary>
|
||||
public int? MaxRequestLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timespan temporary files are kept, before they are removed by a background task.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticTemporaryFileLifeTime)]
|
||||
public TimeSpan TemporaryFileLifeTime { get; set; } = TimeSpan.Parse(StaticTemporaryFileLifeTime);
|
||||
}
|
||||
|
||||
@@ -305,6 +305,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
Services.AddUnique<IMediaTypeService, MediaTypeService>();
|
||||
Services.AddUnique<IFileService, FileService>();
|
||||
Services.AddUnique<ITemplateService, TemplateService>();
|
||||
Services.AddUnique<ITemporaryFileService, TemporaryFileService>();
|
||||
Services.AddUnique<ITemplateContentParserService, TemplateContentParserService>();
|
||||
Services.AddUnique<IEntityService, EntityService>();
|
||||
Services.AddUnique<IRelationService, RelationService>();
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
|
||||
public class CreateTemporaryFileModel : TemporaryFileModelBase
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
|
||||
public class TemporaryFileModel : TemporaryFileModelBase
|
||||
{
|
||||
public required DateTime AvailableUntil { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
|
||||
public abstract class TemporaryFileModelBase
|
||||
{
|
||||
public required string FileName { get; set; }
|
||||
|
||||
public Guid Key { get; set; }
|
||||
|
||||
public Func<Stream> OpenReadStream { get; set; } = () => Stream.Null;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
|
||||
namespace Umbraco.Cms.Core.Persistence.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Persists temporary files.
|
||||
/// </summary>
|
||||
public interface ITemporaryFileRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a temporary file from its key.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique key of the temporary file.</param>
|
||||
/// <returns>The temporary file model if found on that specified key, otherwise null.</returns>
|
||||
Task<TemporaryFileModel?> GetAsync(Guid key);
|
||||
|
||||
/// <summary>
|
||||
/// Creates or update a temporary file.
|
||||
/// </summary>
|
||||
/// <param name="model">The model for the temporary file</param>
|
||||
Task SaveAsync(TemporaryFileModel model);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a temporary file using it's unique key.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique key for the temporary file.</param>
|
||||
Task DeleteAsync(Guid key);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all temporary files that have its TempFileModel.AvailableUntil lower than a specified time.
|
||||
/// </summary>
|
||||
/// <returns>The keys of the delete temporary files.</returns>
|
||||
Task<IEnumerable<Guid>> CleanUpOldTempFiles(DateTime dateTime);
|
||||
}
|
||||
15
src/Umbraco.Core/Services/ITemporaryFileService.cs
Normal file
15
src/Umbraco.Core/Services/ITemporaryFileService.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public interface ITemporaryFileService
|
||||
{
|
||||
Task<Attempt<TemporaryFileModel?, TemporaryFileOperationStatus>> CreateAsync(CreateTemporaryFileModel createModel);
|
||||
|
||||
Task<Attempt<TemporaryFileModel?, TemporaryFileOperationStatus>> DeleteAsync(Guid key);
|
||||
|
||||
Task<TemporaryFileModel?> GetAsync(Guid key);
|
||||
|
||||
Task<IEnumerable<Guid>> CleanUpOldTempFiles();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
public enum TemporaryFileOperationStatus
|
||||
{
|
||||
Success = 0,
|
||||
FileExtensionNotAllowed = 1,
|
||||
KeyAlreadyUsed = 2,
|
||||
NotFound = 3,
|
||||
}
|
||||
85
src/Umbraco.Core/Services/TemporaryFileService.cs
Normal file
85
src/Umbraco.Core/Services/TemporaryFileService.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
internal sealed class TemporaryFileService : ITemporaryFileService
|
||||
{
|
||||
private readonly ITemporaryFileRepository _temporaryFileRepository;
|
||||
private RuntimeSettings _runtimeSettings;
|
||||
private ContentSettings _contentSettings;
|
||||
|
||||
public TemporaryFileService(
|
||||
ITemporaryFileRepository temporaryFileRepository,
|
||||
IOptionsMonitor<RuntimeSettings> runtimeOptionsMonitor,
|
||||
IOptionsMonitor<ContentSettings> contentOptionsMonitor)
|
||||
{
|
||||
_temporaryFileRepository = temporaryFileRepository;
|
||||
|
||||
_runtimeSettings = runtimeOptionsMonitor.CurrentValue;
|
||||
_contentSettings = contentOptionsMonitor.CurrentValue;
|
||||
|
||||
runtimeOptionsMonitor.OnChange(x => _runtimeSettings = x);
|
||||
contentOptionsMonitor.OnChange(x => _contentSettings = x);
|
||||
}
|
||||
|
||||
public async Task<Attempt<TemporaryFileModel?, TemporaryFileOperationStatus>> CreateAsync(CreateTemporaryFileModel createModel)
|
||||
{
|
||||
TemporaryFileOperationStatus validationResult = Validate(createModel);
|
||||
if (validationResult != TemporaryFileOperationStatus.Success)
|
||||
{
|
||||
return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(validationResult, null);
|
||||
}
|
||||
|
||||
TemporaryFileModel? temporaryFileModel = await _temporaryFileRepository.GetAsync(createModel.Key);
|
||||
if (temporaryFileModel is not null)
|
||||
{
|
||||
return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(TemporaryFileOperationStatus.KeyAlreadyUsed, null);
|
||||
}
|
||||
|
||||
temporaryFileModel = new TemporaryFileModel
|
||||
{
|
||||
Key = createModel.Key,
|
||||
FileName = createModel.FileName,
|
||||
OpenReadStream = createModel.OpenReadStream,
|
||||
AvailableUntil = DateTime.Now.Add(_runtimeSettings.TemporaryFileLifeTime)
|
||||
};
|
||||
|
||||
await _temporaryFileRepository.SaveAsync(temporaryFileModel);
|
||||
|
||||
return Attempt<TemporaryFileModel?, TemporaryFileOperationStatus>.Succeed(TemporaryFileOperationStatus.Success, temporaryFileModel);
|
||||
}
|
||||
|
||||
private TemporaryFileOperationStatus Validate(TemporaryFileModelBase temporaryFileModel)
|
||||
=> IsAllowedFileExtension(temporaryFileModel) == false
|
||||
? TemporaryFileOperationStatus.FileExtensionNotAllowed
|
||||
: TemporaryFileOperationStatus.Success;
|
||||
|
||||
private bool IsAllowedFileExtension(TemporaryFileModelBase temporaryFileModel)
|
||||
{
|
||||
var extension = Path.GetExtension(temporaryFileModel.FileName)[1..];
|
||||
|
||||
return _contentSettings.IsFileAllowedForUpload(extension);
|
||||
}
|
||||
|
||||
public async Task<Attempt<TemporaryFileModel?, TemporaryFileOperationStatus>> DeleteAsync(Guid key)
|
||||
{
|
||||
TemporaryFileModel? model = await _temporaryFileRepository.GetAsync(key);
|
||||
if (model is null)
|
||||
{
|
||||
return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(TemporaryFileOperationStatus.NotFound, null);
|
||||
}
|
||||
|
||||
await _temporaryFileRepository.DeleteAsync(key);
|
||||
|
||||
return Attempt<TemporaryFileModel?, TemporaryFileOperationStatus>.Succeed(TemporaryFileOperationStatus.Success, model);
|
||||
}
|
||||
|
||||
public async Task<TemporaryFileModel?> GetAsync(Guid key) => await _temporaryFileRepository.GetAsync(key);
|
||||
|
||||
public async Task<IEnumerable<Guid>> CleanUpOldTempFiles() => await _temporaryFileRepository.CleanUpOldTempFiles(DateTime.Now);
|
||||
}
|
||||
@@ -52,6 +52,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.Services.AddUnique<IServerRegistrationRepository, ServerRegistrationRepository>();
|
||||
builder.Services.AddUnique<ITagRepository, TagRepository>();
|
||||
builder.Services.AddUnique<ITemplateRepository, TemplateRepository>();
|
||||
builder.Services.AddUnique<ITemporaryFileRepository, LocalFileSystemTemporaryFileRepository>();
|
||||
builder.Services.AddUnique<IUserGroupRepository, UserGroupRepository>();
|
||||
builder.Services.AddUnique<IUserRepository, UserRepository>();
|
||||
builder.Services.AddUnique<IConsentRepository, ConsentRepository>();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Umbraco.Cms.Infrastructure.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
public static class TemporaryFileServiceExtensions
|
||||
{
|
||||
public static void EnlistDeleteIfScopeCompletes(this ITemporaryFileService temporaryFileService, Guid temporaryFileKey, IScopeProvider scopeProvider, [CallerMemberName] string memberName = "")
|
||||
{
|
||||
scopeProvider.Context?.Enlist(
|
||||
temporaryFileKey.ToString(),
|
||||
() => memberName,
|
||||
(completed, svc) =>
|
||||
{
|
||||
if (completed)
|
||||
{
|
||||
temporaryFileService.DeleteAsync(temporaryFileKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Runtime;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HostedServices;
|
||||
|
||||
/// <summary>
|
||||
/// Recurring hosted service that executes the temporary file cleanup.
|
||||
/// </summary>
|
||||
public class TemporaryFileCleanup : RecurringHostedServiceBase
|
||||
{
|
||||
private readonly ILogger<TemporaryFileCleanup> _logger;
|
||||
private readonly IMainDom _mainDom;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
private readonly IServerRoleAccessor _serverRoleAccessor;
|
||||
private readonly ITemporaryFileService _service;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TemporaryFileCleanup" /> class.
|
||||
/// </summary>
|
||||
public TemporaryFileCleanup(
|
||||
IRuntimeState runtimeState,
|
||||
ILogger<TemporaryFileCleanup> logger,
|
||||
ITemporaryFileService temporaryFileService,
|
||||
IMainDom mainDom,
|
||||
IServerRoleAccessor serverRoleAccessor)
|
||||
: base(logger, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5))
|
||||
{
|
||||
_runtimeState = runtimeState;
|
||||
_logger = logger;
|
||||
_service = temporaryFileService;
|
||||
_mainDom = mainDom;
|
||||
_serverRoleAccessor = serverRoleAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task PerformExecuteAsync(object? state)
|
||||
{
|
||||
if (_runtimeState.Level != RuntimeLevel.Run)
|
||||
{
|
||||
return Task.FromResult(true); // repeat...
|
||||
}
|
||||
|
||||
// Don't run on replicas nor unknown role servers
|
||||
switch (_serverRoleAccessor.CurrentServerRole)
|
||||
{
|
||||
case ServerRole.Subscriber:
|
||||
_logger.LogDebug("Does not run on subscriber servers");
|
||||
return Task.CompletedTask;
|
||||
case ServerRole.Unknown:
|
||||
_logger.LogDebug("Does not run on servers with unknown role");
|
||||
return Task.CompletedTask;
|
||||
case ServerRole.Single:
|
||||
case ServerRole.SchedulingPublisher:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Ensure we do not run if not main domain, but do NOT lock it
|
||||
if (!_mainDom.IsMainDom)
|
||||
{
|
||||
_logger.LogDebug("Does not run if not MainDom");
|
||||
return Task.FromResult(false); // do NOT repeat, going down
|
||||
}
|
||||
|
||||
var count = _service.CleanUpOldTempFiles().GetAwaiter().GetResult().Count();
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
_logger.LogDebug("Deleted {Count} temporary file(s)", count);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Task complete, no items were deleted");
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.FileProviders.Physical;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
|
||||
|
||||
internal sealed class LocalFileSystemTemporaryFileRepository : ITemporaryFileRepository
|
||||
{
|
||||
private const string MetaDataFileName = ".metadata";
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
private readonly ILogger<LocalFileSystemTemporaryFileRepository> _logger;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
|
||||
public LocalFileSystemTemporaryFileRepository(
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
ILogger<LocalFileSystemTemporaryFileRepository> logger,
|
||||
IJsonSerializer jsonSerializer)
|
||||
{
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_logger = logger;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
}
|
||||
|
||||
private DirectoryInfo GetRootDirectory()
|
||||
{
|
||||
var path = Path.Combine(_hostingEnvironment.LocalTempPath, "TemporaryFile");
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
|
||||
return new DirectoryInfo(path);
|
||||
}
|
||||
|
||||
public async Task<TemporaryFileModel?> GetAsync(Guid key)
|
||||
{
|
||||
|
||||
DirectoryInfo rootDirectory = GetRootDirectory();
|
||||
|
||||
DirectoryInfo? fileDirectory = rootDirectory.GetDirectories(key.ToString()).FirstOrDefault();
|
||||
if (fileDirectory is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
FileInfo[] files = fileDirectory.GetFiles();
|
||||
|
||||
if (files.Length != 2)
|
||||
{
|
||||
_logger.LogError("Unexpected number of files in folder {FolderPath}", fileDirectory.FullName);
|
||||
return null;
|
||||
}
|
||||
|
||||
var (actualFile, metadataFile) = GetFilesByType(files);
|
||||
|
||||
FileMetaData metaData = await GetMetaDataAsync(metadataFile);
|
||||
|
||||
return new TemporaryFileModel()
|
||||
{
|
||||
FileName = actualFile.Name,
|
||||
Key = key,
|
||||
OpenReadStream = () => actualFile.CreateReadStream(),
|
||||
AvailableUntil = metaData.AvailableUntil
|
||||
};
|
||||
}
|
||||
|
||||
public async Task SaveAsync(TemporaryFileModel model)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nameof(model));
|
||||
|
||||
// Ensure folder does not exist
|
||||
await DeleteAsync(model.Key);
|
||||
|
||||
DirectoryInfo rootDirectory = GetRootDirectory();
|
||||
|
||||
DirectoryInfo fileDirectory = rootDirectory.CreateSubdirectory(model.Key.ToString());
|
||||
|
||||
var fullFileName = Path.Combine(fileDirectory.FullName, model.FileName);
|
||||
var metadataFileName = Path.Combine(fileDirectory.FullName, MetaDataFileName);
|
||||
|
||||
await Task.WhenAll(
|
||||
CreateActualFile(model, fullFileName),
|
||||
CreateMetadataFile(metadataFileName, new FileMetaData()
|
||||
{
|
||||
AvailableUntil = model.AvailableUntil
|
||||
}));
|
||||
}
|
||||
|
||||
public Task DeleteAsync(Guid key)
|
||||
{
|
||||
DirectoryInfo rootDirectory = GetRootDirectory();
|
||||
|
||||
DirectoryInfo? fileDirectory = rootDirectory.GetDirectories(key.ToString()).FirstOrDefault();
|
||||
|
||||
if (fileDirectory is not null)
|
||||
{
|
||||
fileDirectory.Delete(true);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Guid>> CleanUpOldTempFiles(DateTime now)
|
||||
{
|
||||
DirectoryInfo rootDirectory = GetRootDirectory();
|
||||
|
||||
var keysToDelete = new List<Guid>();
|
||||
|
||||
foreach (DirectoryInfo fileDirectory in rootDirectory.EnumerateDirectories())
|
||||
{
|
||||
var metadataFileName = Path.Combine(fileDirectory.FullName, MetaDataFileName);
|
||||
FileMetaData metaData = await GetMetaDataAsync(new PhysicalFileInfo(new FileInfo(metadataFileName)));
|
||||
|
||||
if (metaData.AvailableUntil < now)
|
||||
{
|
||||
keysToDelete.Add(Guid.Parse(fileDirectory.Name));
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(keysToDelete.Select(DeleteAsync).ToArray());
|
||||
|
||||
return keysToDelete;
|
||||
}
|
||||
|
||||
private async Task CreateMetadataFile(string fullFilePath, FileMetaData metaData)
|
||||
{
|
||||
var metadataContent = _jsonSerializer.Serialize(metaData);
|
||||
|
||||
await File.WriteAllTextAsync(fullFilePath, metadataContent);
|
||||
}
|
||||
|
||||
private static async Task CreateActualFile(TemporaryFileModel model, string fullFilePath)
|
||||
{
|
||||
FileStream fileStream = File.Create(fullFilePath);
|
||||
await using var dataStream = model.OpenReadStream();
|
||||
dataStream.Seek(0, SeekOrigin.Begin);
|
||||
await dataStream.CopyToAsync(fileStream);
|
||||
fileStream.Close();
|
||||
}
|
||||
|
||||
private async Task<FileMetaData> GetMetaDataAsync(IFileInfo metadataFile)
|
||||
{
|
||||
using var reader = new StreamReader(metadataFile.CreateReadStream());
|
||||
var fileContent = await reader.ReadToEndAsync();
|
||||
FileMetaData? result = _jsonSerializer.Deserialize<FileMetaData>(fileContent);
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
_logger.LogError("Unexpected metadata {FilePath}\n{FileContent}", metadataFile.PhysicalPath, fileContent);
|
||||
throw new InvalidOperationException("Unexpected content");
|
||||
}
|
||||
|
||||
private class FileMetaData
|
||||
{
|
||||
public DateTime AvailableUntil { get; init; }
|
||||
}
|
||||
|
||||
private (IFileInfo actualFile, IFileInfo metadataFile) GetFilesByType(FileInfo[] files) =>
|
||||
files[0].Name == MetaDataFileName
|
||||
? (new PhysicalFileInfo(files[1]), new PhysicalFileInfo(files[0]))
|
||||
: (new PhysicalFileInfo(files[0]), new PhysicalFileInfo(files[1]));
|
||||
}
|
||||
@@ -185,6 +185,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.Services.AddHostedService<KeepAlive>();
|
||||
builder.Services.AddHostedService<LogScrubber>();
|
||||
builder.Services.AddHostedService<ContentVersionCleanup>();
|
||||
builder.Services.AddHostedService<TemporaryFileCleanup>();
|
||||
builder.Services.AddHostedService<ScheduledPublishing>();
|
||||
builder.Services.AddHostedService<TempFileCleanup>();
|
||||
builder.Services.AddHostedService<InstructionProcessTask>();
|
||||
|
||||
Reference in New Issue
Block a user