From 62038ed1bb0d44e6a785474a78aee437eb53f0df Mon Sep 17 00:00:00 2001 From: Benedek Farkas Date: Fri, 3 May 2024 13:19:20 +0000 Subject: [PATCH] #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 --- .../Controllers/AdminController.cs | 38 ++++++++++-- .../Models/ImageProfilePart.cs | 2 +- .../Services/IImageProfileService.cs | 9 ++- .../Services/ImageProfileManager.cs | 52 ++++++++--------- .../Services/ImageProfileService.cs | 58 ++++++++++++++++--- .../Views/Admin/Index.cshtml | 6 +- 6 files changed, 123 insertions(+), 42 deletions(-) diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Controllers/AdminController.cs index 9c92d78940e..ad6ab16f245 100644 --- a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Controllers/AdminController.cs +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Controllers/AdminController.cs @@ -88,7 +88,7 @@ public ActionResult Index(FormCollection input) { if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage media profiles"))) return new HttpUnauthorizedResult(); - var viewModel = new AdminIndexViewModel {ImageProfiles = new List(), Options = new AdminIndexOptions()}; + var viewModel = new AdminIndexViewModel { ImageProfiles = new List(), Options = new AdminIndexOptions() }; UpdateModel(viewModel); var checkedItems = viewModel.ImageProfiles.Where(c => c.IsChecked); @@ -133,7 +133,7 @@ public ActionResult Edit(int id) { Category = f.Category, Type = f.Type, 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 @@ public ActionResult Delete(int id) { return HttpNotFound(); } - Services.ContentManager.Remove(profile.ContentItem); + _profileService.DeleteImageProfile(id); Services.Notifier.Success(T("Image Profile {0} deleted", profile.Name)); return RedirectToAction("Index"); @@ -175,7 +175,7 @@ public ActionResult Move(string direction, int id, int filterId) { throw new ArgumentException("direction"); } - return RedirectToAction("Edit", new {id}); + return RedirectToAction("Edit", new { id }); } public ActionResult Preview(int id) { @@ -185,6 +185,36 @@ public ActionResult Preview(int id) { 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 model, string prefix, string[] includeProperties, string[] excludeProperties) { return TryUpdateModel(model, prefix, includeProperties, excludeProperties); } diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/ImageProfilePart.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/ImageProfilePart.cs index a8de5d8fe8a..1b432036b3c 100644 --- a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/ImageProfilePart.cs +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/ImageProfilePart.cs @@ -24,4 +24,4 @@ public IList FileNames { get { return Record.FileNames; } } } -} \ No newline at end of file +} diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageProfileService.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageProfileService.cs index e3a240f75f0..d8c77cc66bc 100644 --- a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageProfileService.cs +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageProfileService.cs @@ -10,5 +10,12 @@ public interface IImageProfileService : IDependency { void DeleteImageProfile(int id); void MoveUp(int filterId); void MoveDown(int filterId); + bool PurgeImageProfile(int id); + bool PurgeObsoleteImageProfiles(); } -} \ No newline at end of file + + public static class ImageProfileServiceExtensions { + public static string GetNameHashCode(this IImageProfileService service, string name) => + name.GetHashCode().ToString("x").ToLowerInvariant(); + } +} diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProfileManager.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProfileManager.cs index 8d939a524ac..68d4cf4a8e4 100644 --- a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProfileManager.cs +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProfileManager.cs @@ -66,7 +66,7 @@ public string GetImageProfileUrl(string path, string profileName, ContentItem co // path is the publicUrl of the media, so it might contain url-encoded chars // 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; // Before checking everything else, ensure that the content item that needs to be processed has a ImagePart. @@ -79,10 +79,10 @@ public string GetImageProfileUrl(string path, string profileName, ContentItem co if (checkForProfile) { //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)) { - 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)) { - _fileNameProvider.UpdateFileName(profileName, System.Web.HttpUtility.UrlDecode(path), profileFilePath); + _fileNameProvider.UpdateFileName(profileName, HttpUtility.UrlDecode(path), profileFilePath); filePath = profileFilePath; } } @@ -93,28 +93,24 @@ public string GetImageProfileUrl(string path, string profileName, ContentItem co process = true; } - // the processd file doesn't exist anymore, process it else if (!_storageProvider.FileExists(filePath)) { Logger.Debug("Processed file no longer exists, processing required, profile {0} for image {1}", profileName, path); process = true; } - // if the original file is more recent, process it - else { - DateTime pathLastUpdated; - if (TryGetImageLastUpdated(path, out pathLastUpdated)) { - var filePathLastUpdated = _storageProvider.GetFile(filePath).GetLastUpdated(); + else if (TryGetImageLastUpdated(path, out DateTime pathLastUpdated)) { + var filePathLastUpdated = _storageProvider.GetFile(filePath).GetLastUpdated(); - if (pathLastUpdated > filePathLastUpdated) { - Logger.Debug("Original file more recent, processing required, profile {0} for image {1}", profileName, path); + if (pathLastUpdated > filePathLastUpdated) { + 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. if (string.IsNullOrWhiteSpace(filePath)) { filePath = _storageProvider.GetStoragePath(path); @@ -129,9 +125,11 @@ public string GetImageProfileUrl(string path, string profileName, ContentItem co if (customFilters == null || !customFilters.Any(c => c != null)) { profilePart = _profileService.GetImageProfileByName(profileName); - if (profilePart == null) - return String.Empty; - } else { + if (profilePart == null) { + return string.Empty; + } + } + else { profilePart = _services.ContentManager.New("ImageProfile"); profilePart.Name = profileName; foreach (var customFilter in customFilters) { @@ -142,13 +140,13 @@ public string GetImageProfileUrl(string path, string profileName, ContentItem co // 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 // to handle cross machines concurrency - lock (String.Intern(path)) { + lock (string.Intern(path)) { using (var image = GetImage(path)) { if (image == 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(); // if a content item is provided, use it while tokenizing @@ -166,7 +164,7 @@ public string GetImageProfileUrl(string path, string profileName, ContentItem co descriptor.Filter(filterContext); } - _fileNameProvider.UpdateFileName(profileName, System.Web.HttpUtility.UrlDecode(path), filterContext.FilePath); + _fileNameProvider.UpdateFileName(profileName, HttpUtility.UrlDecode(path), filterContext.FilePath); if (!filterContext.Saved) { try { @@ -187,7 +185,8 @@ public string GetImageProfileUrl(string path, string profileName, ContentItem co // the storage provider may have altered the filepath filterContext.FilePath = newFile.GetPath(); } - } catch (Exception e) { + } + catch (Exception e) { Logger.Error(e, "A profile could not be processed: " + path); } } @@ -215,14 +214,14 @@ private Stream GetImage(string path) { try { var file = _storageProvider.GetFile(storagePath); return file.OpenRead(); - } catch (Exception e) { + } + catch (Exception e) { Logger.Error(e, "path:" + path + " storagePath:" + storagePath); } } // http://blob.storage-provider.net/my-image.jpg - Uri absoluteUri; - if (Uri.TryCreate(path, UriKind.Absolute, out absoluteUri)) { + if (Uri.TryCreate(path, UriKind.Absolute, out Uri absoluteUri)) { return new WebClient().OpenRead(absoluteUri); } @@ -248,13 +247,12 @@ private bool TryGetImageLastUpdated(string path, out DateTime lastUpdated) { } private string FormatProfilePath(string profileName, string path) { - var filenameWithExtension = Path.GetFileName(path) ?? ""; var fileLocation = path.Substring(0, path.Length - filenameWithExtension.Length); return _storageProvider.Combine( - _storageProvider.Combine(profileName.GetHashCode().ToString("x").ToLowerInvariant(), fileLocation.GetHashCode().ToString("x").ToLowerInvariant()), - filenameWithExtension); + _storageProvider.Combine(_profileService.GetNameHashCode(profileName), _profileService.GetNameHashCode(fileLocation)), + filenameWithExtension); } } } diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProfileService.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProfileService.cs index 27e34228cdf..20853b6d376 100644 --- a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProfileService.cs +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProfileService.cs @@ -1,31 +1,34 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Orchard.Caching; using Orchard.ContentManagement; using Orchard.Data; -using Orchard.Localization; +using Orchard.FileSystems.Media; +using Orchard.Logging; using Orchard.MediaProcessing.Models; namespace Orchard.MediaProcessing.Services { - public class ImageProfileService : IImageProfileService { + public class ImageProfileService : Component, IImageProfileService { private readonly IContentManager _contentManager; private readonly ICacheManager _cacheManager; private readonly IRepository _filterRepository; private readonly ISignals _signals; + private readonly IStorageProvider _storageProvider; public ImageProfileService( - IContentManager contentManager, + IContentManager contentManager, ICacheManager cacheManager, IRepository filterRepository, - ISignals signals) { + ISignals signals, + IStorageProvider storageProvider) { _contentManager = contentManager; _cacheManager = cacheManager; _filterRepository = filterRepository; _signals = signals; + _storageProvider = storageProvider; } - public Localizer T { get; set; } - public ImageProfilePart GetImageProfile(int id) { return _contentManager.Get(id); } @@ -70,6 +73,7 @@ public void DeleteImageProfile(int id) { var profile = _contentManager.Get(id); if (profile != null) { + DeleteImageProfileFolder(profile.As().Name); _contentManager.Remove(profile); } } @@ -115,5 +119,43 @@ public void MoveDown(int filterId) { next.Position = filter.Position; 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); + } } -} \ No newline at end of file +} diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Admin/Index.cshtml b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Admin/Index.cshtml index bd6efd60a5d..c0b52fe1184 100644 --- a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Admin/Index.cshtml +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Admin/Index.cshtml @@ -14,7 +14,10 @@ @using (Html.BeginFormAntiForgeryPost()) { @Html.ValidationSummary() -
@Html.ActionLink(T("Add a new Media Profile").ToString(), "Create", new { Area = "Contents", id = "ImageProfile", returnurl = HttpContext.Current.Request.RawUrl }, new { @class = "button primaryAction" })
+
+ @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" }) +
@@ -56,6 +59,7 @@ @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("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("Preview").ToString(), "Preview", new { id = entry.ImageProfileId })*@