Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
99963ec
prototype drag and drop upload
madsrasmussen Nov 2, 2022
4bb5865
Add upload image endpoint
Zeegaan Nov 2, 2022
491a68e
Add MediaPickerThreeController.cs
Zeegaan Nov 2, 2022
7e35368
Revert "Add upload image endpoint"
Zeegaan Nov 2, 2022
b20b6f8
Update IIOHelper dependency
Zeegaan Nov 2, 2022
79fd5b9
show preview when uploading a new media item
madsrasmussen Nov 2, 2022
a419529
Merge branch 'v10/feature/media-picker-3-direct-upload' of https://gi…
madsrasmussen Nov 2, 2022
e73e989
open uploaded media in media entry editor
madsrasmussen Nov 2, 2022
d099841
map data from uploaded media entry to support cropper
madsrasmussen Nov 2, 2022
2a07a04
add crop data to uploaded media item
madsrasmussen Nov 2, 2022
0cd150b
remove media library buttons for media entries not created in the med…
madsrasmussen Nov 2, 2022
3cd8e05
Implement temp images save & add to media picker 3
Zeegaan Nov 3, 2022
70f6709
Implement ITemporaryImageService
Zeegaan Nov 3, 2022
75f80f1
Remove save logic from MediaPicker3PropertyEditor
Zeegaan Nov 3, 2022
7a30634
Dont use a TempImageDto
Zeegaan Nov 3, 2022
00095e7
Add GetByAlias endpoint
Zeegaan Nov 3, 2022
c53e90b
Add additonal xml doc
Zeegaan Nov 3, 2022
ae4e1f0
Refactor to take array of aliases
Zeegaan Nov 3, 2022
719c906
Add FromQuery attribute
Zeegaan Nov 3, 2022
18772c7
Formatting
Zeegaan Nov 4, 2022
e084843
add resource to get media types by alias
madsrasmussen Nov 7, 2022
6f8d818
validate file size and file type based on server variables
madsrasmussen Nov 7, 2022
6927642
Update OpenApi.json
Zeegaan Nov 7, 2022
c182ac7
rename endpoint to upload media
Zeegaan Nov 7, 2022
808cefe
Merge branch 'v10/feature/media-picker-3-direct-upload' of https://gi…
madsrasmussen Nov 7, 2022
481abd3
Use baseurl Method
Zeegaan Nov 7, 2022
659bc64
Dont upload in rte folder
Zeegaan Nov 7, 2022
08de796
pass params correctly to end point
madsrasmussen Nov 8, 2022
443c97c
queue files before uploading
madsrasmussen Nov 8, 2022
c84d0bc
handle invalid files
madsrasmussen Nov 8, 2022
0b56967
progress bar design adjustments
madsrasmussen Nov 8, 2022
ea33e36
only create data url for images
madsrasmussen Nov 8, 2022
59c465d
disable edit and name buttons when uploading
madsrasmussen Nov 8, 2022
713d978
fix missing error messages for invalid files
madsrasmussen Nov 9, 2022
d8ce38d
add temp location to media entry
madsrasmussen Nov 9, 2022
84853f7
Add startNode to TemporaryImageService.cs
Zeegaan Nov 9, 2022
2098d35
Refactor get by alias
Zeegaan Nov 9, 2022
853536a
Rename to GetAllFiltered
Zeegaan Nov 9, 2022
adb4d16
use getAllFiltered method
madsrasmussen Nov 9, 2022
b1ffcfd
remove autoselect option
madsrasmussen Nov 9, 2022
3c310c1
fix missing alias when selecting media type
madsrasmussen Nov 9, 2022
fbb6f2d
fix file filter
madsrasmussen Nov 9, 2022
6843ef3
don't overwrite invalid entries from dropping new files
madsrasmussen Nov 9, 2022
95ed911
add disallowed files to filter
madsrasmussen Nov 9, 2022
a10992d
remove console.log
madsrasmussen Nov 9, 2022
8441f32
move media uploader logic to reusable function
madsrasmussen Nov 11, 2022
d2715e9
fix missing tmp location
madsrasmussen Nov 11, 2022
d94da65
attach media type alias to the mediaEntry
madsrasmussen Nov 11, 2022
a4fe75b
support readonly mode
madsrasmussen Nov 11, 2022
5b79103
show discard changes when files has been dropped
madsrasmussen Nov 11, 2022
ce740ac
add disabled prop to button group
madsrasmussen Nov 14, 2022
d13a45d
emit events when upload queue starts and ends
madsrasmussen Nov 14, 2022
46ab224
pass node to media picker property editor
madsrasmussen Nov 14, 2022
e7d86c3
add service to keep track of all uploads in progress
madsrasmussen Nov 14, 2022
2471b3b
add upload in progress to uploadTracker when the queue starts and ends
madsrasmussen Nov 14, 2022
e6d7ad2
disabled buttons when any upload is in progress
madsrasmussen Nov 14, 2022
4a32c71
return a subscription to align with eventsService
madsrasmussen Nov 14, 2022
8977e0c
Merge branch 'v10/dev' into v10/feature/media-picker-3-direct-upload
madsrasmussen Nov 14, 2022
14538dc
Fix up cases where StartNodeId was null
Zeegaan Nov 14, 2022
2fad867
Merge remote-tracking branch 'origin/v10/feature/media-picker-3-direc…
Zeegaan Nov 14, 2022
b62b094
scope css
madsrasmussen Nov 14, 2022
0a24443
Show filename in dialog for selecting media type
madsrasmussen Nov 15, 2022
296dbc3
reuse translation from media library dropzone
madsrasmussen Nov 15, 2022
1ab37fb
Don't check for only images
Zeegaan Nov 15, 2022
4a07a32
Merge remote-tracking branch 'origin/v10/feature/media-picker-3-direc…
Zeegaan Nov 15, 2022
3698b85
Remove composer
Zeegaan Nov 15, 2022
74dc942
Add mediaTypeAlias to TemporaryImageService
Zeegaan Nov 15, 2022
7bfe524
Rename ITemporaryImageService to ITemporaryMediaService
Zeegaan Nov 15, 2022
5d5b438
prefix client side only props with $ so we don't send unnecessary dat…
madsrasmussen Nov 15, 2022
b3a8b5d
use prefixed dataURL in media entry editor
madsrasmussen Nov 15, 2022
f5c2507
render icon for non images
madsrasmussen Nov 15, 2022
7598f5d
fix auto select media type
madsrasmussen Nov 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ private void AddCoreServices()

Services.AddUnique<ICultureImpactFactory>(provider => new CultureImpactFactory(provider.GetRequiredService<IOptionsMonitor<ContentSettings>>()));
Services.AddUnique<IDictionaryService, DictionaryService>();
Services.AddUnique<ITemporaryMediaService, TemporaryMediaService>();
}
}
}
8 changes: 8 additions & 0 deletions src/Umbraco.Core/Services/ITemporaryMediaService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Umbraco.Cms.Core.Models;

namespace Umbraco.Cms.Core.Services;

public interface ITemporaryMediaService
{
public IMedia Save(string temporaryLocation, Guid? startNode, string? mediaTypeAlias);
}
94 changes: 94 additions & 0 deletions src/Umbraco.Core/Services/TemporaryMediaService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;

namespace Umbraco.Cms.Core.Services;

public class TemporaryMediaService : ITemporaryMediaService
{
private readonly IShortStringHelper _shortStringHelper;
private readonly MediaFileManager _mediaFileManager;
private readonly IMediaService _mediaService;
private readonly MediaUrlGeneratorCollection _mediaUrlGenerators;
private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
private readonly IHostEnvironment _hostingEnvironment;
private readonly ILogger<TemporaryMediaService> _logger;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;

public TemporaryMediaService(
IShortStringHelper shortStringHelper,
MediaFileManager mediaFileManager,
IMediaService mediaService,
MediaUrlGeneratorCollection mediaUrlGenerators,
IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
IHostEnvironment hostingEnvironment,
ILogger<TemporaryMediaService> logger,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_shortStringHelper = shortStringHelper;
_mediaFileManager = mediaFileManager;
_mediaService = mediaService;
_mediaUrlGenerators = mediaUrlGenerators;
_contentTypeBaseServiceProvider = contentTypeBaseServiceProvider;
_hostingEnvironment = hostingEnvironment;
_logger = logger;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}

public IMedia Save(string temporaryLocation, Guid? startNode, string? mediaTypeAlias)
{
var userId = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? Constants.Security.SuperUserId;
var absoluteTempImagePath = _hostingEnvironment.MapPathContentRoot(temporaryLocation);
var fileName = Path.GetFileName(absoluteTempImagePath);
var safeFileName = fileName.ToSafeFileName(_shortStringHelper);

var mediaItemName = safeFileName.ToFriendlyName();

IMedia mediaFile;
if (startNode is null)
{
mediaFile = _mediaService.CreateMedia(mediaItemName, Constants.System.Root, mediaTypeAlias ?? Constants.Conventions.MediaTypes.File, userId);
}
else
{
mediaFile = _mediaService.CreateMedia(mediaItemName, startNode.Value, mediaTypeAlias ?? Constants.Conventions.MediaTypes.File, userId);
}

var fileInfo = new FileInfo(absoluteTempImagePath);

FileStream? fileStream = fileInfo.OpenReadWithRetry();
if (fileStream is null)
{
throw new InvalidOperationException("Could not acquire file stream");
}

using (fileStream)
{
mediaFile.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper, _contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream);
}

_mediaService.Save(mediaFile, userId);

// Delete temp file now that we have persisted it
var folderName = Path.GetDirectoryName(absoluteTempImagePath);
try
{
if (folderName is not null)
{
Directory.Delete(folderName, true);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath);
}

return mediaFile;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Runtime.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
Expand Down Expand Up @@ -37,7 +39,8 @@ public MediaPicker3PropertyEditor(
IDataValueEditorFactory dataValueEditorFactory,
IIOHelper ioHelper,
EditorType type = EditorType.PropertyValue)
: this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService<IEditorConfigurationParser>(), type)
: this(dataValueEditorFactory, ioHelper,
StaticServiceProvider.Instance.GetRequiredService<IEditorConfigurationParser>(), type)
{
}

Expand Down Expand Up @@ -68,18 +71,22 @@ internal class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueRefe
{
private readonly IDataTypeService _dataTypeService;
private readonly IJsonSerializer _jsonSerializer;
private readonly ITemporaryMediaService _temporaryMediaService;


public MediaPicker3PropertyValueEditor(
ILocalizedTextService localizedTextService,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute,
IDataTypeService dataTypeService)
IDataTypeService dataTypeService,
ITemporaryMediaService temporaryMediaService)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_jsonSerializer = jsonSerializer;
_dataTypeService = dataTypeService;
_temporaryMediaService = temporaryMediaService;
}

/// <remarks>
Expand Down Expand Up @@ -118,6 +125,11 @@ public override object ToEditor(IProperty property, string? culture = null, stri
{
if (editorValue.Value is JArray dtos)
{
if (editorValue.DataTypeConfiguration is MediaPicker3Configuration configuration)
{
dtos = PersistTempMedia(dtos, configuration);
}

// Clean up redundant/default data
foreach (JObject? dto in dtos.Values<JObject>())
{
Expand Down Expand Up @@ -150,7 +162,7 @@ internal static IEnumerable<MediaWithCropsDto> Deserialize(IJsonSerializer jsonS
Key = Guid.NewGuid(),
MediaKey = guidUdi.Guid,
Crops = Enumerable.Empty<ImageCropperValue.ImageCropperCrop>(),
FocalPoint = new ImageCropperValue.ImageCropperFocalPoint { Left = 0.5m, Top = 0.5m },
FocalPoint = new ImageCropperValue.ImageCropperFocalPoint {Left = 0.5m, Top = 0.5m},
};
}
}
Expand All @@ -170,6 +182,49 @@ internal static IEnumerable<MediaWithCropsDto> Deserialize(IJsonSerializer jsonS
}
}

private JArray PersistTempMedia(JArray jArray, MediaPicker3Configuration mediaPicker3Configuration)
{
var result = new JArray();
foreach (JObject? dto in jArray.Values<JObject>())
{
if (dto is null)
{
continue;
}

if (!dto.TryGetValue("tmpLocation", out JToken? temporaryLocation))
{
// If it does not have a temporary path, it can be an already saved image or not-yet uploaded temp-image, check for media-key
if (dto.TryGetValue("mediaKey", out _))
{
result.Add(dto);
}

continue;
}

var temporaryLocationString = temporaryLocation.Value<string>();
if (temporaryLocationString is null)
{
continue;
}

GuidUdi? startNodeGuid = mediaPicker3Configuration.StartNodeId as GuidUdi ?? null;
JToken? mediaTypeAlias = dto.GetValue("mediaTypeAlias");
IMedia mediaFile = _temporaryMediaService.Save(temporaryLocationString, startNodeGuid?.Guid, mediaTypeAlias?.Value<string>());
MediaWithCropsDto? mediaDto = _jsonSerializer.Deserialize<MediaWithCropsDto>(dto.ToString());
if (mediaDto is null)
{
continue;
}

mediaDto.MediaKey = mediaFile.GetUdi().Guid;
result.Add(JObject.Parse(_jsonSerializer.Serialize(mediaDto)));
}

return result;
}

/// <summary>
/// Model/DTO that represents the JSON that the MediaPicker3 stores.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,10 @@ internal async Task<Dictionary<string, object>> GetServerVariablesAsync()
"propertyTypeApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl<PropertyTypeController>(
controller => controller.HasValues(string.Empty))
},
{
"mediaPickerThreeBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl<MediaPickerThreeController>(
controller => controller.UploadMedia(null!))
},
}
},
{
Expand Down
113 changes: 113 additions & 0 deletions src/Umbraco.Web.BackOffice/Controllers/MediaPickerThreeController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Dictionary;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Web.Common.ActionsResults;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;

namespace Umbraco.Cms.Web.BackOffice.Controllers;

[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
[Authorize(Policy = AuthorizationPolicies.SectionAccessMedia)]
public class MediaPickerThreeController : ContentControllerBase
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly ContentSettings _contentSettings;
private readonly IImageUrlGenerator _imageUrlGenerator;
private readonly IIOHelper _ioHelper;

public MediaPickerThreeController(
ICultureDictionary cultureDictionary,
ILoggerFactory loggerFactory,
IShortStringHelper shortStringHelper,
IEventMessagesFactory eventMessages,
ILocalizedTextService localizedTextService,
IJsonSerializer serializer,
IHostingEnvironment hostingEnvironment,
IOptionsSnapshot<ContentSettings> contentSettings,
IImageUrlGenerator imageUrlGenerator,
IIOHelper ioHelper)
: base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, serializer)
{
_hostingEnvironment = hostingEnvironment;
_contentSettings = contentSettings.Value;
_imageUrlGenerator = imageUrlGenerator;
_ioHelper = ioHelper;
}

[HttpPost]
public async Task<IActionResult> UploadMedia(List<IFormFile> file)
{
// Create an unique folder path to help with concurrent users to avoid filename clash
var imageTempPath =
_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads + "/" + Guid.NewGuid());

// Ensure image temp path exists
if (Directory.Exists(imageTempPath) == false)
{
Directory.CreateDirectory(imageTempPath);
}

// Must have a file
if (file.Count == 0)
{
return NotFound();
}

// Should only have one file
if (file.Count > 1)
{
return new UmbracoProblemResult("Only one file can be uploaded at a time", HttpStatusCode.BadRequest);
}

// Really we should only have one file per request to this endpoint
IFormFile formFile = file.First();

var fileName = formFile.FileName.Trim(new[] { '\"' }).TrimEnd();
var safeFileName = fileName.ToSafeFileName(ShortStringHelper);
var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLowerInvariant();

if (_contentSettings.IsFileAllowedForUpload(ext) == false)
{
// Throw some error - to say can't upload this IMG type
return new UmbracoProblemResult("This is not an image filetype extension that is approved", HttpStatusCode.BadRequest);
}

var newFilePath = imageTempPath + Path.DirectorySeparatorChar + safeFileName;
var relativeNewFilePath = GetRelativePath(newFilePath);

await using (FileStream stream = System.IO.File.Create(newFilePath))
{
await formFile.CopyToAsync(stream);
}

return Ok(new { tmpLocation = relativeNewFilePath });
}

// Use private method istead of _ioHelper.GetRelativePath as that is relative for the webroot and not the content root.
private string GetRelativePath(string path)
{
if (path.IsFullPath())
{
var rootDirectory = _hostingEnvironment.MapPathContentRoot("~");
var relativePath = _ioHelper.PathStartsWith(path, rootDirectory) ? path[rootDirectory.Length..] : path;
path = relativePath;
}

return PathUtility.EnsurePathIsApplicationRootPrefixed(path);
}
}
29 changes: 29 additions & 0 deletions src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,35 @@ public MediaTypeController(
return dto;
}

/// <summary>
/// Returns a media type by alias
/// </summary>
/// /// <param name="alias">Alias of the media type</param>
/// <returns></returns>
public IEnumerable<MediaTypeDisplay> GetAllFiltered([FromQuery] string[] aliases)
{
if (aliases.Length < 1)
{
return _mediaTypeService.GetAll().Select(_umbracoMapper.Map<IMediaType, MediaTypeDisplay>).WhereNotNull();
}

var mediaTypeDisplays = new List<MediaTypeDisplay>();

foreach (var alias in aliases)
{
IMediaType? mediaType = _mediaTypeService.Get(alias);

MediaTypeDisplay? mediaTypeDisplay = _umbracoMapper.Map<IMediaType, MediaTypeDisplay>(mediaType);

if (mediaTypeDisplay is not null)
{
mediaTypeDisplays.Add(mediaTypeDisplay);
}
}

return mediaTypeDisplays;
}

/// <summary>
/// Deletes a media type with a given ID
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ Use this directive to render a button with a dropdown of alternative actions.
size: "@?",
icon: "@?",
label: "@?",
labelKey: "@?"
labelKey: "@?",
disabled: "<?"
},
link: link
};
Expand Down
Loading