Files
Umbraco-CMS/src/Umbraco.Core/Services/FileServiceOperationBase.cs
Kenn Jacobsen e4f9f98f2d File system endpoints redo (#15521)
* First stab at a massive remake of file system based endpoints

* Do not prefix system paths with directory separator char

* Ensure correct and consistent response types

* Fix partial view snippets endpoints

* Clean up IO (path) operations

* Update OpenAPI JSON to match new endpoints

* Return 201 when renaming file system resources

* Add "IsFolder" to file system item endpoints

* Replace "parentPath" with a "parent" object for file system creation endpoints

* Update OpenAPI JSON

* Rewrite snippets

* Regenerate OpenAPI JSON after forward merge

* Remove stylesheet overview endpoint

* Regenerate OpenAPI JSON after forward merge

* add server-file-system module to importmap

* Expose generated resource identifier in 201 responses

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
2024-01-22 08:20:45 +01:00

259 lines
9.2 KiB
C#

using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Services;
public abstract class FileServiceOperationBase<TRepository, TEntity, TOperationStatus> : FileServiceBase<TRepository, TEntity>
where TRepository : IFileRepository, IReadRepository<string, TEntity>, IWriteRepository<TEntity>, IFileWithFoldersRepository
where TEntity : IFile
where TOperationStatus : Enum
{
private readonly ILogger<StylesheetService> _logger;
private readonly IUserIdKeyResolver _userIdKeyResolver;
private readonly IAuditRepository _auditRepository;
protected FileServiceOperationBase(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, TRepository repository, ILogger<StylesheetService> logger, IUserIdKeyResolver userIdKeyResolver, IAuditRepository auditRepository)
: base(provider, loggerFactory, eventMessagesFactory, repository)
{
_logger = logger;
_userIdKeyResolver = userIdKeyResolver;
_auditRepository = auditRepository;
}
protected abstract TOperationStatus Success { get; }
protected abstract TOperationStatus NotFound { get; }
protected abstract TOperationStatus CancelledByNotification { get; }
protected abstract TOperationStatus PathTooLong { get; }
protected abstract TOperationStatus AlreadyExists { get; }
protected abstract TOperationStatus ParentNotFound { get; }
protected abstract TOperationStatus InvalidName { get; }
protected abstract TOperationStatus InvalidFileExtension { get; }
protected abstract string EntityType { get; }
protected abstract SavingNotification<TEntity> SavingNotification(TEntity target, EventMessages messages);
protected abstract SavedNotification<TEntity> SavedNotification(TEntity target, EventMessages messages);
protected abstract DeletingNotification<TEntity> DeletingNotification(TEntity target, EventMessages messages);
protected abstract DeletedNotification<TEntity> DeletedNotification(TEntity target, EventMessages messages);
protected abstract TEntity CreateEntity(string path, string? content);
protected async Task<TOperationStatus> HandleDeleteAsync(string path, Guid userKey)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope();
TEntity? entity = Repository.Get(path);
if (entity is null)
{
return NotFound;
}
EventMessages eventMessages = EventMessagesFactory.Get();
DeletingNotification<TEntity> deletingNotification = DeletingNotification(entity, eventMessages);
if (await scope.Notifications.PublishCancelableAsync(deletingNotification))
{
return CancelledByNotification;
}
Repository.Delete(entity);
scope.Notifications.Publish(DeletedNotification(entity, eventMessages).WithStateFrom(deletingNotification));
await AuditAsync(AuditType.Delete, userKey);
scope.Complete();
return Success;
}
protected async Task<Attempt<TEntity?, TOperationStatus>> HandleCreateAsync(string name, string? parentPath, string? content, Guid userKey)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope();
var path = GetFilePath(parentPath, name);
try
{
TOperationStatus validationResult = ValidateCreate(path);
if (Success.Equals(validationResult) is false)
{
return Attempt.FailWithStatus<TEntity?, TOperationStatus>(validationResult, default);
}
}
catch (PathTooLongException exception)
{
_logger.LogError(exception, "The {EntityType} path was too long", EntityType);
return Attempt.FailWithStatus<TEntity?, TOperationStatus>(PathTooLong, default);
}
TEntity entity = CreateEntity(path, content);
EventMessages eventMessages = EventMessagesFactory.Get();
SavingNotification<TEntity> savingNotification = SavingNotification(entity, eventMessages);
if (await scope.Notifications.PublishCancelableAsync(savingNotification))
{
return Attempt.FailWithStatus<TEntity?, TOperationStatus>(CancelledByNotification, default);
}
Repository.Save(entity);
scope.Notifications.Publish(SavedNotification(entity, eventMessages).WithStateFrom(savingNotification));
await AuditAsync(AuditType.Save, userKey);
scope.Complete();
return Attempt.SucceedWithStatus<TEntity?, TOperationStatus>(Success, entity);
}
protected async Task<Attempt<TEntity?, TOperationStatus>> HandleUpdateAsync(string path, string content, Guid userKey)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope();
TEntity? entity = Repository.Get(path);
if (entity is null)
{
return Attempt.FailWithStatus<TEntity?, TOperationStatus>(NotFound, default);
}
entity.Content = content;
EventMessages eventMessages = EventMessagesFactory.Get();
SavingNotification<TEntity> savingNotification = SavingNotification(entity, eventMessages);
if (await scope.Notifications.PublishCancelableAsync(savingNotification))
{
return Attempt.FailWithStatus<TEntity?, TOperationStatus>(CancelledByNotification, default);
}
Repository.Save(entity);
scope.Notifications.Publish(SavedNotification(entity, eventMessages).WithStateFrom(savingNotification));
await AuditAsync(AuditType.Save, userKey);
scope.Complete();
return Attempt.SucceedWithStatus<TEntity?, TOperationStatus>(Success, entity);
}
protected async Task<Attempt<TEntity?, TOperationStatus>> HandleRenameAsync(string path, string newName, Guid userKey)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope();
TEntity? entity = Repository.Get(path);
if (entity is null)
{
return Attempt.FailWithStatus<TEntity?, TOperationStatus>(NotFound, default);
}
var newPath = ReplaceFileName(path, newName);
try
{
TOperationStatus validationResult = ValidateRename(newName, newPath);
if (Success.Equals(validationResult) is false)
{
return Attempt.FailWithStatus<TEntity?, TOperationStatus>(validationResult, default);
}
}
catch (PathTooLongException exception)
{
_logger.LogError(exception, "The {EntityType} path was too long", EntityType);
return Attempt.FailWithStatus<TEntity?, TOperationStatus>(PathTooLong, default);
}
entity.Path = newPath;
EventMessages eventMessages = EventMessagesFactory.Get();
SavingNotification<TEntity> savingNotification = SavingNotification(entity, eventMessages);
if (await scope.Notifications.PublishCancelableAsync(savingNotification))
{
return Attempt.FailWithStatus<TEntity?, TOperationStatus>(CancelledByNotification, default);
}
Repository.Save(entity);
scope.Notifications.Publish(SavedNotification(entity, eventMessages).WithStateFrom(savingNotification));
await AuditAsync(AuditType.Save, userKey);
scope.Complete();
return Attempt.SucceedWithStatus<TEntity?, TOperationStatus>(Success, entity);
}
private TOperationStatus ValidateCreate(string path)
{
if (Repository.Exists(path))
{
return AlreadyExists;
}
var directoryPath = GetDirectoryPath(path);
if (directoryPath.IsNullOrWhiteSpace() is false && Repository.FolderExists(directoryPath) is false)
{
return ParentNotFound;
}
var fileName = GetFileName(path);
if (HasValidFileName(fileName) is false)
{
return InvalidName;
}
if (HasValidFileExtension(fileName) is false)
{
return InvalidFileExtension;
}
return Success;
}
private TOperationStatus ValidateRename(string newName, string newPath)
{
if (Repository.Exists(newPath))
{
return AlreadyExists;
}
if (HasValidFileName(newName) is false)
{
return InvalidName;
}
if (HasValidFileExtension(newName) is false)
{
return InvalidFileExtension;
}
return Success;
}
private async Task AuditAsync(AuditType type, Guid userKey)
{
var userId = await _userIdKeyResolver.GetAsync(userKey);
_auditRepository.Save(new AuditItem(-1, type, userId, EntityType));
}
private string GetFilePath(string? parentPath, string fileName)
=> Path.Join(parentPath, fileName);
private string ReplaceFileName(string path, string newName)
=> Path.Join(GetDirectoryPath(path), newName);
private string GetDirectoryPath(string path)
=> Path.GetDirectoryName(path) ?? string.Empty;
private string GetFileName(string path)
=> Path.GetFileName(path);
}