Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 10 commits into from
May 3, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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<ImageProfileEntry>(), Options = new AdminIndexOptions()};
var viewModel = new AdminIndexViewModel { ImageProfiles = new List<ImageProfileEntry>(), Options = new AdminIndexOptions() };
UpdateModel(viewModel);

var checkedItems = viewModel.ImageProfiles.Where(c => c.IsChecked);
Expand Down Expand Up @@ -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
});
}
}
Expand All @@ -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");
Expand All @@ -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) {
Expand All @@ -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>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) {
return TryUpdateModel(model, prefix, includeProperties, excludeProperties);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ public IList<FileNameRecord> FileNames {
get { return Record.FileNames; }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}

public static class ImageProfileServiceExtensions {
public static string GetNameHashCode(this IImageProfileService service, string name) =>
name.GetHashCode().ToString("x").ToLowerInvariant();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
}
Expand All @@ -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);
Expand All @@ -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<ImageProfilePart>("ImageProfile");
profilePart.Name = profileName;
foreach (var customFilter in customFilters) {
Expand All @@ -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<string, object>();
// if a content item is provided, use it while tokenizing
Expand All @@ -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 {
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<FilterRecord> _filterRepository;
private readonly ISignals _signals;
private readonly IStorageProvider _storageProvider;

public ImageProfileService(
IContentManager contentManager,
IContentManager contentManager,
ICacheManager cacheManager,
IRepository<FilterRecord> 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<ImageProfilePart>(id);
}
Expand Down Expand Up @@ -70,6 +73,7 @@ public void DeleteImageProfile(int id) {
var profile = _contentManager.Get(id);

if (profile != null) {
DeleteImageProfileFolder(profile.As<ImageProfilePart>().Name);
_contentManager.Remove(profile);
}
}
Expand Down Expand Up @@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@

@using (Html.BeginFormAntiForgeryPost()) {
@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">
<label for="publishActions">@T("Actions:")</label>
Expand Down Expand Up @@ -56,6 +59,7 @@
<td>
@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 })*@
</td>
Expand Down