using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.IO; using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Scoping; using Umbraco.Core.Services.Changes; namespace Umbraco.Core.Services.Implement { /// /// Represents the Media Service, which is an easy access to operations involving /// public class MediaService : ScopeRepositoryService, IMediaService { private readonly IMediaRepository _mediaRepository; private readonly IMediaTypeRepository _mediaTypeRepository; private readonly IAuditRepository _auditRepository; private readonly IEntityRepository _entityRepository; private readonly IMediaFileSystem _mediaFileSystem; #region Constructors public MediaService(IScopeProvider provider, IMediaFileSystem mediaFileSystem, ILogger logger, IEventMessagesFactory eventMessagesFactory, IMediaRepository mediaRepository, IAuditRepository auditRepository, IMediaTypeRepository mediaTypeRepository, IEntityRepository entityRepository) : base(provider, logger, eventMessagesFactory) { _mediaFileSystem = mediaFileSystem; _mediaRepository = mediaRepository; _auditRepository = auditRepository; _mediaTypeRepository = mediaTypeRepository; _entityRepository = entityRepository; } #endregion #region Count public int Count(string mediaTypeAlias = null) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); return _mediaRepository.Count(mediaTypeAlias); } } public int CountNotTrashed(string mediaTypeAlias = null) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); var mediaTypeId = 0; if (string.IsNullOrWhiteSpace(mediaTypeAlias) == false) { var mediaType = _mediaTypeRepository.Get(mediaTypeAlias); if (mediaType == null) return 0; mediaTypeId = mediaType.Id; } var query = Query().Where(x => x.Trashed == false); if (mediaTypeId > 0) query = query.Where(x => x.ContentTypeId == mediaTypeId); return _mediaRepository.Count(query); } } public int CountChildren(int parentId, string mediaTypeAlias = null) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); return _mediaRepository.CountChildren(parentId, mediaTypeAlias); } } public int CountDescendants(int parentId, string mediaTypeAlias = null) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); return _mediaRepository.CountDescendants(parentId, mediaTypeAlias); } } #endregion #region Create /// /// Creates an object using the alias of the /// that this Media should based on. /// /// /// Note that using this method will simply return a new IMedia without any identity /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects /// that does not invoke a save operation against the database. /// /// Name of the Media object /// Id of Parent for the new Media item /// Alias of the /// Optional id of the user creating the media item /// public IMedia CreateMedia(string name, Guid parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId) { var parent = GetById(parentId); return CreateMedia(name, parent, mediaTypeAlias, userId); } /// /// Creates an object of a specified media type. /// /// This method simply returns a new, non-persisted, IMedia without any identity. It /// is intended as a shortcut to creating new media objects that does not invoke a save /// operation against the database. /// /// The name of the media object. /// The identifier of the parent, or -1. /// The alias of the media type. /// The optional id of the user creating the media. /// The media object. public IMedia CreateMedia(string name, int parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId) { var mediaType = GetMediaType(mediaTypeAlias); if (mediaType == null) throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); var parent = parentId > 0 ? GetById(parentId) : null; if (parentId > 0 && parent == null) throw new ArgumentException("No media with that id.", nameof(parentId)); if (name != null && name.Length > 255) { throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); } var media = new Models.Media(name, parentId, mediaType); using (var scope = ScopeProvider.CreateScope()) { CreateMedia(scope, media, parent, userId, false); scope.Complete(); } return media; } /// /// Creates an object of a specified media type, at root. /// /// This method simply returns a new, non-persisted, IMedia without any identity. It /// is intended as a shortcut to creating new media objects that does not invoke a save /// operation against the database. /// /// The name of the media object. /// The alias of the media type. /// The optional id of the user creating the media. /// The media object. public IMedia CreateMedia(string name, string mediaTypeAlias, int userId = Constants.Security.SuperUserId) { // not locking since not saving anything var mediaType = GetMediaType(mediaTypeAlias); if (mediaType == null) throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); if (name != null && name.Length > 255) { throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); } var media = new Models.Media(name, -1, mediaType); using (var scope = ScopeProvider.CreateScope()) { CreateMedia(scope, media, null, userId, false); scope.Complete(); } return media; } /// /// Creates an object of a specified media type, under a parent. /// /// This method simply returns a new, non-persisted, IMedia without any identity. It /// is intended as a shortcut to creating new media objects that does not invoke a save /// operation against the database. /// /// The name of the media object. /// The parent media object. /// The alias of the media type. /// The optional id of the user creating the media. /// The media object. public IMedia CreateMedia(string name, IMedia parent, string mediaTypeAlias, int userId = Constants.Security.SuperUserId) { if (parent == null) throw new ArgumentNullException(nameof(parent)); using (var scope = ScopeProvider.CreateScope()) { // not locking since not saving anything var mediaType = GetMediaType(mediaTypeAlias); if (mediaType == null) throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback if (name != null && name.Length > 255) { throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); } var media = new Models.Media(name, parent, mediaType); CreateMedia(scope, media, parent, userId, false); scope.Complete(); return media; } } /// /// Creates an object of a specified media type. /// /// This method returns a new, persisted, IMedia with an identity. /// The name of the media object. /// The identifier of the parent, or -1. /// The alias of the media type. /// The optional id of the user creating the media. /// The media object. public IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId) { using (var scope = ScopeProvider.CreateScope()) { // locking the media tree secures media types too scope.WriteLock(Constants.Locks.MediaTree); var mediaType = GetMediaType(mediaTypeAlias); // + locks if (mediaType == null) throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback var parent = parentId > 0 ? GetById(parentId) : null; // + locks if (parentId > 0 && parent == null) throw new ArgumentException("No media with that id.", nameof(parentId)); // causes rollback var media = parentId > 0 ? new Models.Media(name, parent, mediaType) : new Models.Media(name, parentId, mediaType); CreateMedia(scope, media, parent, userId, true); scope.Complete(); return media; } } /// /// Creates an object of a specified media type, under a parent. /// /// This method returns a new, persisted, IMedia with an identity. /// The name of the media object. /// The parent media object. /// The alias of the media type. /// The optional id of the user creating the media. /// The media object. public IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = Constants.Security.SuperUserId) { if (parent == null) throw new ArgumentNullException(nameof(parent)); using (var scope = ScopeProvider.CreateScope()) { // locking the media tree secures media types too scope.WriteLock(Constants.Locks.MediaTree); var mediaType = GetMediaType(mediaTypeAlias); // + locks if (mediaType == null) throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback var media = new Models.Media(name, parent, mediaType); CreateMedia(scope, media, parent, userId, true); scope.Complete(); return media; } } private void CreateMedia(IScope scope, Models.Media media, IMedia parent, int userId, bool withIdentity) { media.CreatorId = userId; if (withIdentity) { // if saving is cancelled, media remains without an identity var saveEventArgs = new SaveEventArgs(media); if (Saving.IsRaisedEventCancelled(saveEventArgs, this)) return; _mediaRepository.Save(media); saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); scope.Events.Dispatch(TreeChanged, this, new TreeChange(media, TreeChangeTypes.RefreshNode).ToEventArgs()); } if (withIdentity == false) return; Audit(AuditType.New, media.CreatorId, media.Id, $"Media '{media.Name}' was created with Id {media.Id}"); } #endregion #region Get, Has, Is /// /// Gets an object by Id /// /// Id of the Media to retrieve /// public IMedia GetById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); return _mediaRepository.Get(id); } } /// /// Gets an object by Id /// /// Ids of the Media to retrieve /// public IEnumerable GetByIds(IEnumerable ids) { var idsA = ids.ToArray(); if (idsA.Length == 0) return Enumerable.Empty(); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); return _mediaRepository.GetMany(idsA); } } /// /// Gets an object by its 'UniqueId' /// /// Guid key of the Media to retrieve /// public IMedia GetById(Guid key) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); return _mediaRepository.Get(key); } } /// /// Gets an object by Id /// /// Ids of the Media to retrieve /// public IEnumerable GetByIds(IEnumerable ids) { var idsA = ids.ToArray(); if (idsA.Length == 0) return Enumerable.Empty(); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); return _mediaRepository.GetMany(idsA); } } /// public IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords, IQuery filter = null, Ordering ordering = null) { if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); if (ordering == null) ordering = Ordering.By("sortOrder"); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); return _mediaRepository.GetPage( Query().Where(x => x.ContentTypeId == contentTypeId), pageIndex, pageSize, out totalRecords, filter, ordering); } } /// public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery filter = null, Ordering ordering = null) { if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); if (ordering == null) ordering = Ordering.By("sortOrder"); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); return _mediaRepository.GetPage( Query().Where(x => contentTypeIds.Contains(x.ContentTypeId)), pageIndex, pageSize, out totalRecords, filter, ordering); } } /// /// Gets a collection of objects by Level /// /// The level to retrieve Media from /// An Enumerable list of objects /// Contrary to most methods, this method filters out trashed media items. public IEnumerable GetByLevel(int level) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); var query = Query().Where(x => x.Level == level && x.Trashed == false); return _mediaRepository.Get(query); } } /// /// Gets a specific version of an item. /// /// Id of the version to retrieve /// An item public IMedia GetVersion(int versionId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); return _mediaRepository.GetVersion(versionId); } } /// /// Gets a collection of an objects versions by Id /// /// /// An Enumerable list of objects public IEnumerable GetVersions(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); return _mediaRepository.GetAllVersions(id); } } /// /// Gets a collection of objects, which are ancestors of the current media. /// /// Id of the to retrieve ancestors for /// An Enumerable list of objects public IEnumerable GetAncestors(int id) { // intentionally not locking var media = GetById(id); return GetAncestors(media); } /// /// Gets a collection of objects, which are ancestors of the current media. /// /// to retrieve ancestors for /// An Enumerable list of objects public IEnumerable GetAncestors(IMedia media) { //null check otherwise we get exceptions if (media.Path.IsNullOrWhiteSpace()) return Enumerable.Empty(); var rootId = Constants.System.RootString; var ids = media.Path.Split(',') .Where(x => x != rootId && x != media.Id.ToString(CultureInfo.InvariantCulture)) .Select(int.Parse) .ToArray(); if (ids.Any() == false) return new List(); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); return _mediaRepository.GetMany(ids); } } /// public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, IQuery filter = null, Ordering ordering = null) { if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); if (ordering == null) ordering = Ordering.By("sortOrder"); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); var query = Query().Where(x => x.ParentId == id); return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); } } /// public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, IQuery filter = null, Ordering ordering = null) { if (ordering == null) ordering = Ordering.By("Path"); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); //if the id is System Root, then just get all if (id != Constants.System.Root) { var mediaPath = _entityRepository.GetAllPaths(Constants.ObjectTypes.Media, id).ToArray(); if (mediaPath.Length == 0) { totalChildren = 0; return Enumerable.Empty(); } return GetPagedLocked(GetPagedDescendantQuery(mediaPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering); } return GetPagedLocked(GetPagedDescendantQuery(null), pageIndex, pageSize, out totalChildren, filter, ordering); } } private IQuery GetPagedDescendantQuery(string mediaPath) { var query = Query(); if (!mediaPath.IsNullOrWhiteSpace()) query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar)); return query; } private IEnumerable GetPagedLocked(IQuery query, long pageIndex, int pageSize, out long totalChildren, IQuery filter, Ordering ordering) { if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); if (ordering == null) throw new ArgumentNullException(nameof(ordering)); return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); } /// /// Gets the parent of the current media as an item. /// /// Id of the to retrieve the parent from /// Parent object public IMedia GetParent(int id) { // intentionally not locking var media = GetById(id); return GetParent(media); } /// /// Gets the parent of the current media as an item. /// /// to retrieve the parent from /// Parent object public IMedia GetParent(IMedia media) { if (media.ParentId == Constants.System.Root || media.ParentId == Constants.System.RecycleBinMedia) return null; return GetById(media.ParentId); } /// /// Gets a collection of objects, which reside at the first level / root /// /// An Enumerable list of objects public IEnumerable GetRootMedia() { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); var query = Query().Where(x => x.ParentId == Constants.System.Root); return _mediaRepository.Get(query); } } /// public IEnumerable GetPagedMediaInRecycleBin(long pageIndex, int pageSize, out long totalRecords, IQuery filter = null, Ordering ordering = null) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { if (ordering == null) ordering = Ordering.By("Path"); scope.ReadLock(Constants.Locks.MediaTree); var query = Query().Where(x => x.Path.StartsWith(Constants.System.RecycleBinMediaPathPrefix)); return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering); } } /// /// Checks whether an item has any children /// /// Id of the /// True if the media has any children otherwise False public bool HasChildren(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.ParentId == id); var count = _mediaRepository.Count(query); return count > 0; } } /// /// Gets an object from the path stored in the 'umbracoFile' property. /// /// Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg) /// public IMedia GetMediaByPath(string mediaPath) { using (ScopeProvider.CreateScope(autoComplete: true)) { return _mediaRepository.GetMediaByPath(mediaPath); } } #endregion #region Save /// /// Saves a single object /// /// The to save /// Id of the User saving the Media /// Optional boolean indicating whether or not to raise events. public Attempt Save(IMedia media, int userId = Constants.Security.SuperUserId, bool raiseEvents = true) { var evtMsgs = EventMessagesFactory.Get(); using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(media, evtMsgs); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) { scope.Complete(); return OperationResult.Attempt.Cancel(evtMsgs); } // poor man's validation? if (string.IsNullOrWhiteSpace(media.Name)) throw new ArgumentException("Media has no name.", nameof(media)); if (media.Name != null && media.Name.Length > 255) { throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); } scope.WriteLock(Constants.Locks.MediaTree); if (media.HasIdentity == false) media.CreatorId = userId; _mediaRepository.Save(media); if (raiseEvents) { saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); } var changeType = TreeChangeTypes.RefreshNode; scope.Events.Dispatch(TreeChanged, this, new TreeChange(media, changeType).ToEventArgs()); Audit(AuditType.Save, userId, media.Id); scope.Complete(); } return OperationResult.Attempt.Succeed(evtMsgs); } /// /// Saves a collection of objects /// /// Collection of to save /// Id of the User saving the Media /// Optional boolean indicating whether or not to raise events. public Attempt Save(IEnumerable medias, int userId = Constants.Security.SuperUserId, bool raiseEvents = true) { var evtMsgs = EventMessagesFactory.Get(); var mediasA = medias.ToArray(); using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(mediasA, evtMsgs); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, new SaveEventArgs(mediasA, evtMsgs))) { scope.Complete(); return OperationResult.Attempt.Cancel(evtMsgs); } var treeChanges = mediasA.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)); scope.WriteLock(Constants.Locks.MediaTree); foreach (var media in mediasA) { if (media.HasIdentity == false) media.CreatorId = userId; _mediaRepository.Save(media); } if (raiseEvents) { saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); } scope.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs()); Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Bulk save media"); scope.Complete(); } return OperationResult.Attempt.Succeed(evtMsgs); } #endregion #region Delete /// /// Permanently deletes an object /// /// The to delete /// Id of the User deleting the Media public Attempt Delete(IMedia media, int userId = Constants.Security.SuperUserId) { var evtMsgs = EventMessagesFactory.Get(); using (var scope = ScopeProvider.CreateScope()) { if (scope.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(media, evtMsgs))) { scope.Complete(); return OperationResult.Attempt.Cancel(evtMsgs); } scope.WriteLock(Constants.Locks.MediaTree); DeleteLocked(scope, media); scope.Events.Dispatch(TreeChanged, this, new TreeChange(media, TreeChangeTypes.Remove).ToEventArgs()); Audit(AuditType.Delete, userId, media.Id); scope.Complete(); } return OperationResult.Attempt.Succeed(evtMsgs); } private void DeleteLocked(IScope scope, IMedia media) { void DoDelete(IMedia c) { _mediaRepository.Delete(c); var args = new DeleteEventArgs(c, false); // raise event & get flagged files scope.Events.Dispatch(Deleted, this, args); // media files deleted by QueuingEventDispatcher } const int pageSize = 500; var page = 0; var total = long.MaxValue; while (page * pageSize < total) { //get descendants - ordered from deepest to shallowest var descendants = GetPagedDescendants(media.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending)); foreach (var c in descendants) DoDelete(c); } DoDelete(media); } //TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way // Delete does - for a good reason: the file may be referenced by other, non-deleted, versions. BUT, // if that's not the case, then the file will never be deleted, because when we delete the media, // the version referencing the file will not be there anymore. SO, we can leak files. /// /// Permanently deletes versions from an object prior to a specific date. /// This method will never delete the latest version of a media item. /// /// Id of the object to delete versions from /// Latest version date /// Optional Id of the User deleting versions of a Media object public void DeleteVersions(int id, DateTime versionDate, int userId = Constants.Security.SuperUserId) { using (var scope = ScopeProvider.CreateScope()) { DeleteVersions(scope, true, id, versionDate, userId); scope.Complete(); //if (uow.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, dateToRetain: versionDate))) //{ // uow.Complete(); // return; //} //uow.WriteLock(Constants.Locks.MediaTree); //var repository = uow.CreateRepository(); //repository.DeleteVersions(id, versionDate); //uow.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate)); //Audit(uow, AuditType.Delete, "Delete Media by version date, userId, Constants.System.Root); //uow.Complete(); } } private void DeleteVersions(IScope scope, bool wlock, int id, DateTime versionDate, int userId = Constants.Security.SuperUserId) { var args = new DeleteRevisionsEventArgs(id, dateToRetain: versionDate); if (scope.Events.DispatchCancelable(DeletingVersions, this, args)) return; if (wlock) scope.WriteLock(Constants.Locks.MediaTree); _mediaRepository.DeleteVersions(id, versionDate); args.CanCancel = false; scope.Events.Dispatch(DeletedVersions, this, args); Audit(AuditType.Delete, userId, Constants.System.Root, "Delete Media by version date"); } /// /// Permanently deletes specific version(s) from an object. /// This method will never delete the latest version of a media item. /// /// Id of the object to delete a version from /// Id of the version to delete /// Boolean indicating whether to delete versions prior to the versionId /// Optional Id of the User deleting versions of a Media object public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Constants.Security.SuperUserId) { using (var scope = ScopeProvider.CreateScope()) { var args = new DeleteRevisionsEventArgs(id, /*specificVersion:*/ versionId); if (scope.Events.DispatchCancelable(DeletingVersions, this, args)) { scope.Complete(); return; } if (deletePriorVersions) { var media = GetVersion(versionId); DeleteVersions(scope, true, id, media.UpdateDate, userId); } else { scope.WriteLock(Constants.Locks.MediaTree); } _mediaRepository.DeleteVersion(versionId); args.CanCancel = false; scope.Events.Dispatch(DeletedVersions, this, args); Audit(AuditType.Delete, userId, Constants.System.Root, "Delete Media by version"); scope.Complete(); } } #endregion #region Move, RecycleBin /// /// Deletes an object by moving it to the Recycle Bin /// /// The to delete /// Id of the User deleting the Media public Attempt MoveToRecycleBin(IMedia media, int userId = Constants.Security.SuperUserId) { var evtMsgs = EventMessagesFactory.Get(); var moves = new List<(IMedia, string)>(); using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MediaTree); // TODO: missing 7.6 "ensure valid path" thing here? // but then should be in PerformMoveLocked on every moved item? var originalPath = media.Path; var moveEventInfo = new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia); var moveEventArgs = new MoveEventArgs(true, evtMsgs, moveEventInfo); if (scope.Events.DispatchCancelable(Trashing, this, moveEventArgs, nameof(Trashing))) { scope.Complete(); return OperationResult.Attempt.Cancel(evtMsgs); } PerformMoveLocked(media, Constants.System.RecycleBinMedia, null, userId, moves, true); scope.Events.Dispatch(TreeChanged, this, new TreeChange(media, TreeChangeTypes.RefreshBranch).ToEventArgs()); var moveInfo = moves.Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) .ToArray(); moveEventArgs.MoveInfoCollection = moveInfo; moveEventArgs.CanCancel = false; scope.Events.Dispatch(Trashed, this, moveEventArgs, nameof(Trashed)); Audit(AuditType.Move, userId, media.Id, "Move Media to recycle bin"); scope.Complete(); } return OperationResult.Attempt.Succeed(evtMsgs); } /// /// Moves an object to a new location /// /// The to move /// Id of the Media's new Parent /// Id of the User moving the Media public Attempt Move(IMedia media, int parentId, int userId = Constants.Security.SuperUserId) { var evtMsgs = EventMessagesFactory.Get(); // if moving to the recycle bin then use the proper method if (parentId == Constants.System.RecycleBinMedia) { MoveToRecycleBin(media, userId); return OperationResult.Attempt.Succeed(evtMsgs); } var moves = new List<(IMedia, string)>(); using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MediaTree); var parent = parentId == Constants.System.Root ? null : GetById(parentId); if (parentId != Constants.System.Root && (parent == null || parent.Trashed)) throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback var moveEventInfo = new MoveEventInfo(media, media.Path, parentId); var moveEventArgs = new MoveEventArgs(true, evtMsgs, moveEventInfo); if (scope.Events.DispatchCancelable(Moving, this, moveEventArgs, nameof(Moving))) { scope.Complete(); return OperationResult.Attempt.Cancel(evtMsgs); } // if media was trashed, and since we're not moving to the recycle bin, // indicate that the trashed status should be changed to false, else just // leave it unchanged var trashed = media.Trashed ? false : (bool?)null; PerformMoveLocked(media, parentId, parent, userId, moves, trashed); scope.Events.Dispatch(TreeChanged, this, new TreeChange(media, TreeChangeTypes.RefreshBranch).ToEventArgs()); var moveInfo = moves //changes .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) .ToArray(); moveEventArgs.MoveInfoCollection = moveInfo; moveEventArgs.CanCancel = false; scope.Events.Dispatch(Moved, this, moveEventArgs, nameof(Moved)); Audit(AuditType.Move, userId, media.Id); scope.Complete(); } return OperationResult.Attempt.Succeed(evtMsgs); } // MUST be called from within WriteLock // trash indicates whether we are trashing, un-trashing, or not changing anything private void PerformMoveLocked(IMedia media, int parentId, IMedia parent, int userId, ICollection<(IMedia, string)> moves, bool? trash) { media.ParentId = parentId; // get the level delta (old pos to new pos) // note that recycle bin (id:-20) level is 0! var levelDelta = 1 - media.Level + (parent?.Level ?? 0); var paths = new Dictionary(); moves.Add((media, media.Path)); // capture original path //need to store the original path to lookup descendants based on it below var originalPath = media.Path; // these will be updated by the repo because we changed parentId //media.Path = (parent == null ? "-1" : parent.Path) + "," + media.Id; //media.SortOrder = ((MediaRepository) repository).NextChildSortOrder(parentId); //media.Level += levelDelta; PerformMoveMediaLocked(media, userId, trash); // if uow is not immediate, content.Path will be updated only when the UOW commits, // and because we want it now, we have to calculate it by ourselves //paths[media.Id] = media.Path; paths[media.Id] = (parent == null ? (parentId == Constants.System.RecycleBinMedia ? "-1,-21" : Constants.System.RootString) : parent.Path) + "," + media.Id; const int pageSize = 500; var query = GetPagedDescendantQuery(originalPath); long total; do { // We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); foreach (var descendant in descendants) { moves.Add((descendant, descendant.Path)); // capture original path // update path and level since we do not update parentId descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; descendant.Level += levelDelta; PerformMoveMediaLocked(descendant, userId, trash); } } while (total > pageSize); } private void PerformMoveMediaLocked(IMedia media, int userId, bool? trash) { if (trash.HasValue) ((ContentBase)media).Trashed = trash.Value; _mediaRepository.Save(media); } /// /// Empties the Recycle Bin by deleting all that resides in the bin /// [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] public OperationResult EmptyRecycleBin() => EmptyRecycleBin(Constants.Security.SuperUserId); /// /// Empties the Recycle Bin by deleting all that resides in the bin /// /// Optional Id of the User emptying the Recycle Bin public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId) { var nodeObjectType = Constants.ObjectTypes.Media; var deleted = new List(); var evtMsgs = EventMessagesFactory.Get(); // TODO: and then? using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MediaTree); // no idea what those events are for, keep a simplified version // v7 EmptyingRecycleBin and EmptiedRecycleBin events are greatly simplified since // each deleted items will have its own deleting/deleted events. so, files and such // are managed by Delete, and not here. var args = new RecycleBinEventArgs(nodeObjectType, evtMsgs); if (scope.Events.DispatchCancelable(EmptyingRecycleBin, this, args)) { scope.Complete(); return OperationResult.Cancel(evtMsgs); } // emptying the recycle bin means deleting whatever is in there - do it properly! var query = Query().Where(x => x.ParentId == Constants.System.RecycleBinMedia); var medias = _mediaRepository.Get(query).ToArray(); foreach (var media in medias) { DeleteLocked(scope, media); deleted.Add(media); } args.CanCancel = false; scope.Events.Dispatch(EmptiedRecycleBin, this, args); scope.Events.Dispatch(TreeChanged, this, deleted.Select(x => new TreeChange(x, TreeChangeTypes.Remove)).ToEventArgs()); Audit(AuditType.Delete, userId, Constants.System.RecycleBinMedia, "Empty Media recycle bin"); scope.Complete(); } return OperationResult.Succeed(evtMsgs); } #endregion #region Others /// /// Sorts a collection of objects by updating the SortOrder according /// to the ordering of items in the passed in . /// /// /// /// /// True if sorting succeeded, otherwise False public bool Sort(IEnumerable items, int userId = Constants.Security.SuperUserId, bool raiseEvents = true) { var itemsA = items.ToArray(); if (itemsA.Length == 0) return true; using (var scope = ScopeProvider.CreateScope()) { var args = new SaveEventArgs(itemsA); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, args)) { scope.Complete(); return false; } var saved = new List(); scope.WriteLock(Constants.Locks.MediaTree); var sortOrder = 0; foreach (var media in itemsA) { // if the current sort order equals that of the media we don't // need to update it, so just increment the sort order and continue. if (media.SortOrder == sortOrder) { sortOrder++; continue; } // else update media.SortOrder = sortOrder++; // save saved.Add(media); _mediaRepository.Save(media); } if (raiseEvents) { args.CanCancel = false; scope.Events.Dispatch(Saved, this, args); } scope.Events.Dispatch(TreeChanged, this, saved.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)).ToEventArgs()); Audit(AuditType.Sort, userId, 0); scope.Complete(); } return true; } public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.WriteLock(Constants.Locks.MediaTree); var report = _mediaRepository.CheckDataIntegrity(options); if (report.FixedIssues.Count > 0) { //The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref var root = new Models.Media("root", -1, new MediaType(-1)) { Id = -1, Key = Guid.Empty }; scope.Events.Dispatch(TreeChanged, this, new TreeChange.EventArgs(new TreeChange(root, TreeChangeTypes.RefreshAll))); } return report; } } #endregion #region Private Methods private void Audit(AuditType type, int userId, int objectId, string message = null) { _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Media), message)); } #endregion #region File Management public Stream GetMediaFileContentStream(string filepath) { if (_mediaFileSystem.FileExists(filepath) == false) return null; try { return _mediaFileSystem.OpenFile(filepath); } catch { return null; // deal with race conds } } public void SetMediaFileContent(string filepath, Stream stream) { _mediaFileSystem.AddFile(filepath, stream, true); } public void DeleteMediaFile(string filepath) { _mediaFileSystem.DeleteFile(filepath); } public long GetMediaFileSize(string filepath) { return _mediaFileSystem.GetSize(filepath); } #endregion #region Event Handlers /// /// Occurs before Delete /// public static event TypedEventHandler> Deleting; /// /// Occurs after Delete /// public static event TypedEventHandler> Deleted; /// /// Occurs before Delete Versions /// public static event TypedEventHandler DeletingVersions; /// /// Occurs after Delete Versions /// public static event TypedEventHandler DeletedVersions; /// /// Occurs before Save /// public static event TypedEventHandler> Saving; /// /// Occurs after Save /// public static event TypedEventHandler> Saved; /// /// Occurs before Media is moved to Recycle Bin /// public static event TypedEventHandler> Trashing; /// /// Occurs after Media is moved to Recycle Bin /// public static event TypedEventHandler> Trashed; /// /// Occurs before Move /// public static event TypedEventHandler> Moving; /// /// Occurs after Move /// public static event TypedEventHandler> Moved; /// /// Occurs before the Recycle Bin is emptied /// public static event TypedEventHandler EmptyingRecycleBin; /// /// Occurs after the Recycle Bin has been Emptied /// public static event TypedEventHandler EmptiedRecycleBin; /// /// Occurs after change. /// internal static event TypedEventHandler.EventArgs> TreeChanged; #endregion #region Content Types /// /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. /// /// /// This needs extra care and attention as its potentially a dangerous and extensive operation. /// Deletes media items of the specified type, and only that type. Does *not* handle content types /// inheritance and compositions, which need to be managed outside of this method. /// /// Id of the /// Optional id of the user deleting the media public void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = Constants.Security.SuperUserId) { // TODO: This currently this is called from the ContentTypeService but that needs to change, // if we are deleting a content type, we should just delete the data and do this operation slightly differently. // This method will recursively go lookup every content item, check if any of it's descendants are // of a different type, move them to the recycle bin, then permanently delete the content items. // The main problem with this is that for every content item being deleted, events are raised... // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. var changes = new List>(); var moves = new List<(IMedia, string)>(); var mediaTypeIdsA = mediaTypeIds.ToArray(); using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MediaTree); var query = Query().WhereIn(x => x.ContentTypeId, mediaTypeIdsA); var medias = _mediaRepository.Get(query).ToArray(); if (scope.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(medias))) { scope.Complete(); return; } // order by level, descending, so deepest first - that way, we cannot move // a media of the deleted type, to the recycle bin (and then delete it...) foreach (var media in medias.OrderByDescending(x => x.ParentId)) { // if current media has children, move them to trash var m = media; var childQuery = Query().Where(x => x.Path.StartsWith(m.Path)); var children = _mediaRepository.Get(childQuery); foreach (var child in children.Where(x => mediaTypeIdsA.Contains(x.ContentTypeId) == false)) { // see MoveToRecycleBin PerformMoveLocked(child, Constants.System.RecycleBinMedia, null, userId, moves, true); changes.Add(new TreeChange(media, TreeChangeTypes.RefreshBranch)); } // delete media // triggers the deleted event (and handles the files) DeleteLocked(scope, media); changes.Add(new TreeChange(media, TreeChangeTypes.Remove)); } var moveInfos = moves.Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) .ToArray(); if (moveInfos.Length > 0) scope.Events.Dispatch(Trashed, this, new MoveEventArgs(false, moveInfos), nameof(Trashed)); scope.Events.Dispatch(TreeChanged, this, changes.ToEventArgs()); Audit(AuditType.Delete, userId, Constants.System.Root, $"Delete Media of types {string.Join(",", mediaTypeIdsA)}"); scope.Complete(); } } /// /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. /// /// This needs extra care and attention as its potentially a dangerous and extensive operation /// Id of the /// Optional id of the user deleting the media public void DeleteMediaOfType(int mediaTypeId, int userId = Constants.Security.SuperUserId) { DeleteMediaOfTypes(new[] { mediaTypeId }, userId); } private IMediaType GetMediaType(string mediaTypeAlias) { if (mediaTypeAlias == null) throw new ArgumentNullException(nameof(mediaTypeAlias)); if (string.IsNullOrWhiteSpace(mediaTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(mediaTypeAlias)); using (var scope = ScopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MediaTypes); var query = Query().Where(x => x.Alias == mediaTypeAlias); var mediaType = _mediaTypeRepository.Get(query).FirstOrDefault(); if (mediaType == null) throw new InvalidOperationException($"No media type matched the specified alias '{mediaTypeAlias}'."); scope.Complete(); return mediaType; } } #endregion } }