#6981: Normalized image profile path hash and added profile purging (#8788)

* #6981 Normalized image profile path hash and added profile purging

* Profile purge functions rather belong in IImageProfileService

* Deleting an Image Profile now also removes all its files too

* Comment formatting

* Caching the value of the "Orchard.MediaProcessing.NormalizePath" app setting in ImageProfileManager

* Code styling in ImageProfileManager

* Formatting and code styling ImageProfileManager

---------

Co-authored-by: Arjan Noordende <arjan@zumey.com>
This commit is contained in:
Benedek Farkas 2024-05-03 13:19:20 +00:00 committed by GitHub
parent 6eab0a1260
commit 62038ed1bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 123 additions and 42 deletions

View File

@ -88,7 +88,7 @@ namespace Orchard.MediaProcessing.Controllers {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage media profiles"))) if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage media profiles")))
return new HttpUnauthorizedResult(); return new HttpUnauthorizedResult();
var viewModel = new AdminIndexViewModel {ImageProfiles = new List<ImageProfileEntry>(), Options = new AdminIndexOptions()}; var viewModel = new AdminIndexViewModel { ImageProfiles = new List<ImageProfileEntry>(), Options = new AdminIndexOptions() };
UpdateModel(viewModel); UpdateModel(viewModel);
var checkedItems = viewModel.ImageProfiles.Where(c => c.IsChecked); var checkedItems = viewModel.ImageProfiles.Where(c => c.IsChecked);
@ -133,7 +133,7 @@ namespace Orchard.MediaProcessing.Controllers {
Category = f.Category, Category = f.Category,
Type = f.Type, Type = f.Type,
FilterRecordId = filter.Id, FilterRecordId = filter.Id,
DisplayText = String.IsNullOrWhiteSpace(filter.Description) ? f.Display(new FilterContext {State = FormParametersHelper.ToDynamic(filter.State)}).Text : filter.Description DisplayText = String.IsNullOrWhiteSpace(filter.Description) ? f.Display(new FilterContext { State = FormParametersHelper.ToDynamic(filter.State) }).Text : filter.Description
}); });
} }
} }
@ -154,7 +154,7 @@ namespace Orchard.MediaProcessing.Controllers {
return HttpNotFound(); return HttpNotFound();
} }
Services.ContentManager.Remove(profile.ContentItem); _profileService.DeleteImageProfile(id);
Services.Notifier.Success(T("Image Profile {0} deleted", profile.Name)); Services.Notifier.Success(T("Image Profile {0} deleted", profile.Name));
return RedirectToAction("Index"); return RedirectToAction("Index");
@ -175,7 +175,7 @@ namespace Orchard.MediaProcessing.Controllers {
throw new ArgumentException("direction"); throw new ArgumentException("direction");
} }
return RedirectToAction("Edit", new {id}); return RedirectToAction("Edit", new { id });
} }
public ActionResult Preview(int id) { public ActionResult Preview(int id) {
@ -185,6 +185,36 @@ namespace Orchard.MediaProcessing.Controllers {
throw new NotImplementedException(); throw new NotImplementedException();
} }
[HttpPost]
public ActionResult Purge(int id) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage media profiles")))
return new HttpUnauthorizedResult();
if (_profileService.PurgeImageProfile(id)) {
Services.Notifier.Information(T("The Image Profile has been purged"));
}
else {
Services.Notifier.Warning(T("Unable to purge the Image Profile, it may already have been purged"));
}
return RedirectToAction("Index");
}
[HttpPost]
public ActionResult PurgeObsolete() {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage media profiles")))
return new HttpUnauthorizedResult();
if (_profileService.PurgeObsoleteImageProfiles()) {
Services.Notifier.Information(T("The obsolete Image Profiles have been purged"));
}
else {
Services.Notifier.Warning(T("Unable to purge the obsolete Image Profiles"));
}
return RedirectToAction("Index");
}
bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) { bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) {
return TryUpdateModel(model, prefix, includeProperties, excludeProperties); return TryUpdateModel(model, prefix, includeProperties, excludeProperties);
} }

View File

@ -24,4 +24,4 @@ namespace Orchard.MediaProcessing.Models {
get { return Record.FileNames; } get { return Record.FileNames; }
} }
} }
} }

View File

@ -10,5 +10,12 @@ namespace Orchard.MediaProcessing.Services {
void DeleteImageProfile(int id); void DeleteImageProfile(int id);
void MoveUp(int filterId); void MoveUp(int filterId);
void MoveDown(int filterId); void MoveDown(int filterId);
bool PurgeImageProfile(int id);
bool PurgeObsoleteImageProfiles();
} }
}
public static class ImageProfileServiceExtensions {
public static string GetNameHashCode(this IImageProfileService service, string name) =>
name.GetHashCode().ToString("x").ToLowerInvariant();
}
}

View File

@ -66,7 +66,7 @@ namespace Orchard.MediaProcessing.Services {
// path is the publicUrl of the media, so it might contain url-encoded chars // path is the publicUrl of the media, so it might contain url-encoded chars
// try to load the processed filename from cache // try to load the processed filename from cache
var filePath = _fileNameProvider.GetFileName(profileName, System.Web.HttpUtility.UrlDecode(path)); var filePath = _fileNameProvider.GetFileName(profileName, HttpUtility.UrlDecode(path));
bool process = false; bool process = false;
// Before checking everything else, ensure that the content item that needs to be processed has a ImagePart. // Before checking everything else, ensure that the content item that needs to be processed has a ImagePart.
@ -79,10 +79,10 @@ namespace Orchard.MediaProcessing.Services {
if (checkForProfile) { if (checkForProfile) {
//after reboot the app cache is empty so we reload the image in the cache if it exists in the _Profiles folder //after reboot the app cache is empty so we reload the image in the cache if it exists in the _Profiles folder
if (string.IsNullOrEmpty(filePath)) { if (string.IsNullOrEmpty(filePath)) {
var profileFilePath = _storageProvider.Combine("_Profiles", FormatProfilePath(profileName, System.Web.HttpUtility.UrlDecode(path))); var profileFilePath = _storageProvider.Combine("_Profiles", FormatProfilePath(profileName, HttpUtility.UrlDecode(path)));
if (_storageProvider.FileExists(profileFilePath)) { if (_storageProvider.FileExists(profileFilePath)) {
_fileNameProvider.UpdateFileName(profileName, System.Web.HttpUtility.UrlDecode(path), profileFilePath); _fileNameProvider.UpdateFileName(profileName, HttpUtility.UrlDecode(path), profileFilePath);
filePath = profileFilePath; filePath = profileFilePath;
} }
} }
@ -93,28 +93,24 @@ namespace Orchard.MediaProcessing.Services {
process = true; process = true;
} }
// the processd file doesn't exist anymore, process it // the processd file doesn't exist anymore, process it
else if (!_storageProvider.FileExists(filePath)) { else if (!_storageProvider.FileExists(filePath)) {
Logger.Debug("Processed file no longer exists, processing required, profile {0} for image {1}", profileName, path); Logger.Debug("Processed file no longer exists, processing required, profile {0} for image {1}", profileName, path);
process = true; process = true;
} }
// if the original file is more recent, process it // if the original file is more recent, process it
else { else if (TryGetImageLastUpdated(path, out DateTime pathLastUpdated)) {
DateTime pathLastUpdated; var filePathLastUpdated = _storageProvider.GetFile(filePath).GetLastUpdated();
if (TryGetImageLastUpdated(path, out pathLastUpdated)) {
var filePathLastUpdated = _storageProvider.GetFile(filePath).GetLastUpdated();
if (pathLastUpdated > filePathLastUpdated) { if (pathLastUpdated > filePathLastUpdated) {
Logger.Debug("Original file more recent, processing required, profile {0} for image {1}", profileName, path); Logger.Debug("Original file more recent, processing required, profile {0} for image {1}", profileName, path);
process = true; process = true;
}
} }
} }
} else { }
else {
// Since media with no ImagePart have no profile, filePath is null, so it's set again to its original path on the storage provider. // Since media with no ImagePart have no profile, filePath is null, so it's set again to its original path on the storage provider.
if (string.IsNullOrWhiteSpace(filePath)) { if (string.IsNullOrWhiteSpace(filePath)) {
filePath = _storageProvider.GetStoragePath(path); filePath = _storageProvider.GetStoragePath(path);
@ -129,9 +125,11 @@ namespace Orchard.MediaProcessing.Services {
if (customFilters == null || !customFilters.Any(c => c != null)) { if (customFilters == null || !customFilters.Any(c => c != null)) {
profilePart = _profileService.GetImageProfileByName(profileName); profilePart = _profileService.GetImageProfileByName(profileName);
if (profilePart == null) if (profilePart == null) {
return String.Empty; return string.Empty;
} else { }
}
else {
profilePart = _services.ContentManager.New<ImageProfilePart>("ImageProfile"); profilePart = _services.ContentManager.New<ImageProfilePart>("ImageProfile");
profilePart.Name = profileName; profilePart.Name = profileName;
foreach (var customFilter in customFilters) { foreach (var customFilter in customFilters) {
@ -142,13 +140,13 @@ namespace Orchard.MediaProcessing.Services {
// prevent two requests from processing the same file at the same time // prevent two requests from processing the same file at the same time
// this is only thread safe at the machine level, so there is a try/catch later // this is only thread safe at the machine level, so there is a try/catch later
// to handle cross machines concurrency // to handle cross machines concurrency
lock (String.Intern(path)) { lock (string.Intern(path)) {
using (var image = GetImage(path)) { using (var image = GetImage(path)) {
if (image == null) { if (image == null) {
return null; return null;
} }
var filterContext = new FilterContext { Media = image, FilePath = _storageProvider.Combine("_Profiles", FormatProfilePath(profileName, System.Web.HttpUtility.UrlDecode(path))) }; var filterContext = new FilterContext { Media = image, FilePath = _storageProvider.Combine("_Profiles", FormatProfilePath(profileName, HttpUtility.UrlDecode(path))) };
var tokens = new Dictionary<string, object>(); var tokens = new Dictionary<string, object>();
// if a content item is provided, use it while tokenizing // if a content item is provided, use it while tokenizing
@ -166,7 +164,7 @@ namespace Orchard.MediaProcessing.Services {
descriptor.Filter(filterContext); descriptor.Filter(filterContext);
} }
_fileNameProvider.UpdateFileName(profileName, System.Web.HttpUtility.UrlDecode(path), filterContext.FilePath); _fileNameProvider.UpdateFileName(profileName, HttpUtility.UrlDecode(path), filterContext.FilePath);
if (!filterContext.Saved) { if (!filterContext.Saved) {
try { try {
@ -187,7 +185,8 @@ namespace Orchard.MediaProcessing.Services {
// the storage provider may have altered the filepath // the storage provider may have altered the filepath
filterContext.FilePath = newFile.GetPath(); filterContext.FilePath = newFile.GetPath();
} }
} catch (Exception e) { }
catch (Exception e) {
Logger.Error(e, "A profile could not be processed: " + path); Logger.Error(e, "A profile could not be processed: " + path);
} }
} }
@ -215,14 +214,14 @@ namespace Orchard.MediaProcessing.Services {
try { try {
var file = _storageProvider.GetFile(storagePath); var file = _storageProvider.GetFile(storagePath);
return file.OpenRead(); return file.OpenRead();
} catch (Exception e) { }
catch (Exception e) {
Logger.Error(e, "path:" + path + " storagePath:" + storagePath); Logger.Error(e, "path:" + path + " storagePath:" + storagePath);
} }
} }
// http://blob.storage-provider.net/my-image.jpg // http://blob.storage-provider.net/my-image.jpg
Uri absoluteUri; if (Uri.TryCreate(path, UriKind.Absolute, out Uri absoluteUri)) {
if (Uri.TryCreate(path, UriKind.Absolute, out absoluteUri)) {
return new WebClient().OpenRead(absoluteUri); return new WebClient().OpenRead(absoluteUri);
} }
@ -248,13 +247,12 @@ namespace Orchard.MediaProcessing.Services {
} }
private string FormatProfilePath(string profileName, string path) { private string FormatProfilePath(string profileName, string path) {
var filenameWithExtension = Path.GetFileName(path) ?? ""; var filenameWithExtension = Path.GetFileName(path) ?? "";
var fileLocation = path.Substring(0, path.Length - filenameWithExtension.Length); var fileLocation = path.Substring(0, path.Length - filenameWithExtension.Length);
return _storageProvider.Combine( return _storageProvider.Combine(
_storageProvider.Combine(profileName.GetHashCode().ToString("x").ToLowerInvariant(), fileLocation.GetHashCode().ToString("x").ToLowerInvariant()), _storageProvider.Combine(_profileService.GetNameHashCode(profileName), _profileService.GetNameHashCode(fileLocation)),
filenameWithExtension); filenameWithExtension);
} }
} }
} }

View File

@ -1,31 +1,34 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Orchard.Caching; using Orchard.Caching;
using Orchard.ContentManagement; using Orchard.ContentManagement;
using Orchard.Data; using Orchard.Data;
using Orchard.Localization; using Orchard.FileSystems.Media;
using Orchard.Logging;
using Orchard.MediaProcessing.Models; using Orchard.MediaProcessing.Models;
namespace Orchard.MediaProcessing.Services { namespace Orchard.MediaProcessing.Services {
public class ImageProfileService : IImageProfileService { public class ImageProfileService : Component, IImageProfileService {
private readonly IContentManager _contentManager; private readonly IContentManager _contentManager;
private readonly ICacheManager _cacheManager; private readonly ICacheManager _cacheManager;
private readonly IRepository<FilterRecord> _filterRepository; private readonly IRepository<FilterRecord> _filterRepository;
private readonly ISignals _signals; private readonly ISignals _signals;
private readonly IStorageProvider _storageProvider;
public ImageProfileService( public ImageProfileService(
IContentManager contentManager, IContentManager contentManager,
ICacheManager cacheManager, ICacheManager cacheManager,
IRepository<FilterRecord> filterRepository, IRepository<FilterRecord> filterRepository,
ISignals signals) { ISignals signals,
IStorageProvider storageProvider) {
_contentManager = contentManager; _contentManager = contentManager;
_cacheManager = cacheManager; _cacheManager = cacheManager;
_filterRepository = filterRepository; _filterRepository = filterRepository;
_signals = signals; _signals = signals;
_storageProvider = storageProvider;
} }
public Localizer T { get; set; }
public ImageProfilePart GetImageProfile(int id) { public ImageProfilePart GetImageProfile(int id) {
return _contentManager.Get<ImageProfilePart>(id); return _contentManager.Get<ImageProfilePart>(id);
} }
@ -70,6 +73,7 @@ namespace Orchard.MediaProcessing.Services {
var profile = _contentManager.Get(id); var profile = _contentManager.Get(id);
if (profile != null) { if (profile != null) {
DeleteImageProfileFolder(profile.As<ImageProfilePart>().Name);
_contentManager.Remove(profile); _contentManager.Remove(profile);
} }
} }
@ -115,5 +119,43 @@ namespace Orchard.MediaProcessing.Services {
next.Position = filter.Position; next.Position = filter.Position;
filter.Position = temp; filter.Position = temp;
} }
public bool PurgeImageProfile(int id) {
var profile = GetImageProfile(id);
try {
DeleteImageProfileFolder(profile.Name);
profile.FileNames.Clear();
_signals.Trigger("MediaProcessing_Saved_" + profile.Name);
return true;
}
catch (Exception ex) {
Logger.Warning(ex, "Unable to purge image profile '{0}'", profile.Name);
return false;
}
}
public bool PurgeObsoleteImageProfiles() {
var profiles = GetAllImageProfiles();
try {
if (profiles != null) {
var validPaths = profiles.Select(profile => _storageProvider.Combine("_Profiles", this.GetNameHashCode(profile.Name)));
foreach (var folder in _storageProvider.ListFolders("_Profiles").Select(f => f.GetPath())) {
if (!validPaths.Any(folder.StartsWith)) {
_storageProvider.DeleteFolder(folder);
}
}
}
return true;
}
catch (Exception ex) {
Logger.Warning(ex, "Unable to purge obsolete image profiles");
return false;
}
}
private void DeleteImageProfileFolder(string profileName) {
var folder = _storageProvider.Combine("_Profiles", this.GetNameHashCode(profileName));
_storageProvider.DeleteFolder(folder);
}
} }
} }

View File

@ -14,7 +14,10 @@
@using (Html.BeginFormAntiForgeryPost()) { @using (Html.BeginFormAntiForgeryPost()) {
@Html.ValidationSummary() @Html.ValidationSummary()
<div class="manage">@Html.ActionLink(T("Add a new Media Profile").ToString(), "Create", new { Area = "Contents", id = "ImageProfile", returnurl = HttpContext.Current.Request.RawUrl }, new { @class = "button primaryAction" })</div> <div class="manage">
@Html.ActionLink(T("Purge Obsolete").ToString(), "PurgeObsolete", null, new { itemprop = "UnsafeUrl", @class = "button remove", data_unsafe_url = @T("Are you sure you wish to purge all obsolete profile images and force all dynamic profile images to be regenerated?").ToString() })
@Html.ActionLink(T("Add a new Media Profile").ToString(), "Create", new { Area = "Contents", id = "ImageProfile", returnurl = HttpContext.Current.Request.RawUrl }, new { @class = "button primaryAction" })
</div>
<fieldset class="bulk-actions"> <fieldset class="bulk-actions">
<label for="publishActions">@T("Actions:")</label> <label for="publishActions">@T("Actions:")</label>
@ -56,6 +59,7 @@
<td> <td>
@Html.ActionLink(T("Properties").ToString(), "Edit", new { Area = "Contents", id = entry.ImageProfileId, returnurl = HttpContext.Current.Request.RawUrl }) | @Html.ActionLink(T("Properties").ToString(), "Edit", new { Area = "Contents", id = entry.ImageProfileId, returnurl = HttpContext.Current.Request.RawUrl }) |
@Html.ActionLink(T("Edit").ToString(), "Edit", new { id = entry.ImageProfileId }) | @Html.ActionLink(T("Edit").ToString(), "Edit", new { id = entry.ImageProfileId }) |
@Html.ActionLink(T("Purge").ToString(), "Purge", new { id = entry.ImageProfileId }, new { itemprop = "UnsafeUrl", data_unsafe_url = @T("Are you sure you wish to purge all images for this profile?").ToString() }) |
@Html.ActionLink(T("Delete").ToString(), "Delete", new { id = entry.ImageProfileId }, new { itemprop = "RemoveUrl UnsafeUrl" }) @Html.ActionLink(T("Delete").ToString(), "Delete", new { id = entry.ImageProfileId }, new { itemprop = "RemoveUrl UnsafeUrl" })
@*@Html.ActionLink(T("Preview").ToString(), "Preview", new { id = entry.ImageProfileId })*@ @*@Html.ActionLink(T("Preview").ToString(), "Preview", new { id = entry.ImageProfileId })*@
</td> </td>