1364 lines
58 KiB
C#
1364 lines
58 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Umbraco.Core.Events;
|
|
using Umbraco.Core.Exceptions;
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Represents the Media Service, which is an easy access to operations involving <see cref="IMedia"/>
|
|
/// </summary>
|
|
public class MediaService : ScopeRepositoryService, IMediaService
|
|
{
|
|
private readonly IMediaRepository _mediaRepository;
|
|
private readonly IMediaTypeRepository _mediaTypeRepository;
|
|
private readonly IAuditRepository _auditRepository;
|
|
private readonly IEntityRepository _entityRepository;
|
|
|
|
private readonly MediaFileSystem _mediaFileSystem;
|
|
|
|
#region Constructors
|
|
|
|
public MediaService(IScopeProvider provider, MediaFileSystem 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<IMedia>().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
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="IMedia"/> object using the alias of the <see cref="IMediaType"/>
|
|
/// that this Media should based on.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
/// <param name="name">Name of the Media object</param>
|
|
/// <param name="parentId">Id of Parent for the new Media item</param>
|
|
/// <param name="mediaTypeAlias">Alias of the <see cref="IMediaType"/></param>
|
|
/// <param name="userId">Optional id of the user creating the media item</param>
|
|
/// <returns><see cref="IMedia"/></returns>
|
|
public IMedia CreateMedia(string name, Guid parentId, string mediaTypeAlias, int userId = 0)
|
|
{
|
|
var parent = GetById(parentId);
|
|
return CreateMedia(name, parent, mediaTypeAlias, userId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="IMedia"/> object of a specified media type.
|
|
/// </summary>
|
|
/// <remarks>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.
|
|
/// </remarks>
|
|
/// <param name="name">The name of the media object.</param>
|
|
/// <param name="parentId">The identifier of the parent, or -1.</param>
|
|
/// <param name="mediaTypeAlias">The alias of the media type.</param>
|
|
/// <param name="userId">The optional id of the user creating the media.</param>
|
|
/// <returns>The media object.</returns>
|
|
public IMedia CreateMedia(string name, int parentId, string mediaTypeAlias, int userId = 0)
|
|
{
|
|
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));
|
|
|
|
var media = new Models.Media(name, parentId, mediaType);
|
|
using (var scope = ScopeProvider.CreateScope())
|
|
{
|
|
CreateMedia(scope, media, parent, userId, false);
|
|
scope.Complete();
|
|
}
|
|
|
|
return media;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="IMedia"/> object of a specified media type, at root.
|
|
/// </summary>
|
|
/// <remarks>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.
|
|
/// </remarks>
|
|
/// <param name="name">The name of the media object.</param>
|
|
/// <param name="mediaTypeAlias">The alias of the media type.</param>
|
|
/// <param name="userId">The optional id of the user creating the media.</param>
|
|
/// <returns>The media object.</returns>
|
|
public IMedia CreateMedia(string name, string mediaTypeAlias, int userId = 0)
|
|
{
|
|
// not locking since not saving anything
|
|
|
|
var mediaType = GetMediaType(mediaTypeAlias);
|
|
if (mediaType == null)
|
|
throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias));
|
|
|
|
var media = new Models.Media(name, -1, mediaType);
|
|
using (var scope = ScopeProvider.CreateScope())
|
|
{
|
|
CreateMedia(scope, media, null, userId, false);
|
|
scope.Complete();
|
|
}
|
|
|
|
return media;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="IMedia"/> object of a specified media type, under a parent.
|
|
/// </summary>
|
|
/// <remarks>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.
|
|
/// </remarks>
|
|
/// <param name="name">The name of the media object.</param>
|
|
/// <param name="parent">The parent media object.</param>
|
|
/// <param name="mediaTypeAlias">The alias of the media type.</param>
|
|
/// <param name="userId">The optional id of the user creating the media.</param>
|
|
/// <returns>The media object.</returns>
|
|
public IMedia CreateMedia(string name, IMedia parent, string mediaTypeAlias, int userId = 0)
|
|
{
|
|
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 // causes rollback
|
|
|
|
var media = new Models.Media(name, parent, mediaType);
|
|
CreateMedia(scope, media, parent, userId, false);
|
|
|
|
scope.Complete();
|
|
return media;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="IMedia"/> object of a specified media type.
|
|
/// </summary>
|
|
/// <remarks>This method returns a new, persisted, IMedia with an identity.</remarks>
|
|
/// <param name="name">The name of the media object.</param>
|
|
/// <param name="parentId">The identifier of the parent, or -1.</param>
|
|
/// <param name="mediaTypeAlias">The alias of the media type.</param>
|
|
/// <param name="userId">The optional id of the user creating the media.</param>
|
|
/// <returns>The media object.</returns>
|
|
public IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = 0)
|
|
{
|
|
using (var scope = ScopeProvider.CreateScope())
|
|
{
|
|
// locking the media tree secures media types too
|
|
scope.WriteLock(Constants.Locks.MediaTree);
|
|
|
|
var mediaType = GetMediaType(mediaTypeAlias); // + locks // + locks
|
|
if (mediaType == null)
|
|
throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback // causes rollback
|
|
|
|
var parent = parentId > 0 ? GetById(parentId) : null; // + locks // + locks
|
|
if (parentId > 0 && parent == null)
|
|
throw new ArgumentException("No media with that id.", nameof(parentId)); // causes rollback // 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="IMedia"/> object of a specified media type, under a parent.
|
|
/// </summary>
|
|
/// <remarks>This method returns a new, persisted, IMedia with an identity.</remarks>
|
|
/// <param name="name">The name of the media object.</param>
|
|
/// <param name="parent">The parent media object.</param>
|
|
/// <param name="mediaTypeAlias">The alias of the media type.</param>
|
|
/// <param name="userId">The optional id of the user creating the media.</param>
|
|
/// <returns>The media object.</returns>
|
|
public IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = 0)
|
|
{
|
|
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 // + locks
|
|
if (mediaType == null)
|
|
throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback // 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<IMedia>(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<IMedia>(media, TreeChangeTypes.RefreshNode).ToEventArgs());
|
|
}
|
|
|
|
scope.Events.Dispatch(Created, this, new NewEventArgs<IMedia>(media, false, media.ContentType.Alias, parent));
|
|
|
|
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
|
|
|
|
/// <summary>
|
|
/// Gets an <see cref="IMedia"/> object by Id
|
|
/// </summary>
|
|
/// <param name="id">Id of the Media to retrieve</param>
|
|
/// <returns><see cref="IMedia"/></returns>
|
|
public IMedia GetById(int id)
|
|
{
|
|
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
|
{
|
|
scope.ReadLock(Constants.Locks.MediaTree);
|
|
return _mediaRepository.Get(id);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an <see cref="IMedia"/> object by Id
|
|
/// </summary>
|
|
/// <param name="ids">Ids of the Media to retrieve</param>
|
|
/// <returns><see cref="IMedia"/></returns>
|
|
public IEnumerable<IMedia> GetByIds(IEnumerable<int> ids)
|
|
{
|
|
var idsA = ids.ToArray();
|
|
if (idsA.Length == 0) return Enumerable.Empty<IMedia>();
|
|
|
|
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
|
{
|
|
scope.ReadLock(Constants.Locks.MediaTree);
|
|
return _mediaRepository.GetMany(idsA);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an <see cref="IMedia"/> object by its 'UniqueId'
|
|
/// </summary>
|
|
/// <param name="key">Guid key of the Media to retrieve</param>
|
|
/// <returns><see cref="IMedia"/></returns>
|
|
public IMedia GetById(Guid key)
|
|
{
|
|
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
|
{
|
|
scope.ReadLock(Constants.Locks.MediaTree);
|
|
return _mediaRepository.Get(key);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an <see cref="IMedia"/> object by Id
|
|
/// </summary>
|
|
/// <param name="ids">Ids of the Media to retrieve</param>
|
|
/// <returns><see cref="IMedia"/></returns>
|
|
public IEnumerable<IMedia> GetByIds(IEnumerable<Guid> ids)
|
|
{
|
|
var idsA = ids.ToArray();
|
|
if (idsA.Length == 0) return Enumerable.Empty<IMedia>();
|
|
|
|
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
|
{
|
|
scope.ReadLock(Constants.Locks.MediaTree);
|
|
return _mediaRepository.GetMany(idsA);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<IMedia> GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords, IQuery<IMedia> 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<IMedia>().Where(x => x.ContentTypeId == contentTypeId),
|
|
pageIndex, pageSize, out totalRecords, filter, ordering);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<IMedia> GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery<IMedia> 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<IMedia>().Where(x => contentTypeIds.Contains(x.ContentTypeId)),
|
|
pageIndex, pageSize, out totalRecords, filter, ordering);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IMedia"/> objects by Level
|
|
/// </summary>
|
|
/// <param name="level">The level to retrieve Media from</param>
|
|
/// <returns>An Enumerable list of <see cref="IMedia"/> objects</returns>
|
|
/// <remarks>Contrary to most methods, this method filters out trashed media items.</remarks>
|
|
public IEnumerable<IMedia> GetByLevel(int level)
|
|
{
|
|
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
|
{
|
|
scope.ReadLock(Constants.Locks.MediaTree);
|
|
var query = Query<IMedia>().Where(x => x.Level == level && x.Trashed == false);
|
|
return _mediaRepository.Get(query);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a specific version of an <see cref="IMedia"/> item.
|
|
/// </summary>
|
|
/// <param name="versionId">Id of the version to retrieve</param>
|
|
/// <returns>An <see cref="IMedia"/> item</returns>
|
|
public IMedia GetVersion(int versionId)
|
|
{
|
|
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
|
{
|
|
scope.ReadLock(Constants.Locks.MediaTree);
|
|
return _mediaRepository.GetVersion(versionId);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of an <see cref="IMedia"/> objects versions by Id
|
|
/// </summary>
|
|
/// <param name="id"></param>
|
|
/// <returns>An Enumerable list of <see cref="IMedia"/> objects</returns>
|
|
public IEnumerable<IMedia> GetVersions(int id)
|
|
{
|
|
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
|
{
|
|
scope.ReadLock(Constants.Locks.MediaTree);
|
|
return _mediaRepository.GetAllVersions(id);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IMedia"/> objects, which are ancestors of the current media.
|
|
/// </summary>
|
|
/// <param name="id">Id of the <see cref="IMedia"/> to retrieve ancestors for</param>
|
|
/// <returns>An Enumerable list of <see cref="IMedia"/> objects</returns>
|
|
public IEnumerable<IMedia> GetAncestors(int id)
|
|
{
|
|
// intentionnaly not locking
|
|
var media = GetById(id);
|
|
return GetAncestors(media);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IMedia"/> objects, which are ancestors of the current media.
|
|
/// </summary>
|
|
/// <param name="media"><see cref="IMedia"/> to retrieve ancestors for</param>
|
|
/// <returns>An Enumerable list of <see cref="IMedia"/> objects</returns>
|
|
public IEnumerable<IMedia> GetAncestors(IMedia media)
|
|
{
|
|
//null check otherwise we get exceptions
|
|
if (media.Path.IsNullOrWhiteSpace()) return Enumerable.Empty<IMedia>();
|
|
|
|
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<IMedia>();
|
|
|
|
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
|
{
|
|
scope.ReadLock(Constants.Locks.MediaTree);
|
|
return _mediaRepository.GetMany(ids);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<IMedia> GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren,
|
|
IQuery<IMedia> 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<IMedia>().Where(x => x.ParentId == id);
|
|
return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<IMedia> GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren,
|
|
IQuery<IMedia> 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<IMedia>();
|
|
}
|
|
return GetPagedDescendantsLocked(mediaPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering);
|
|
}
|
|
return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
|
|
}
|
|
}
|
|
|
|
private IEnumerable<IMedia> GetPagedDescendantsLocked(string mediaPath, long pageIndex, int pageSize, out long totalChildren,
|
|
IQuery<IMedia> 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));
|
|
|
|
var query = Query<IMedia>();
|
|
if (!mediaPath.IsNullOrWhiteSpace())
|
|
query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar));
|
|
|
|
return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the parent of the current media as an <see cref="IMedia"/> item.
|
|
/// </summary>
|
|
/// <param name="id">Id of the <see cref="IMedia"/> to retrieve the parent from</param>
|
|
/// <returns>Parent <see cref="IMedia"/> object</returns>
|
|
public IMedia GetParent(int id)
|
|
{
|
|
// intentionnaly not locking
|
|
var media = GetById(id);
|
|
return GetParent(media);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the parent of the current media as an <see cref="IMedia"/> item.
|
|
/// </summary>
|
|
/// <param name="media"><see cref="IMedia"/> to retrieve the parent from</param>
|
|
/// <returns>Parent <see cref="IMedia"/> object</returns>
|
|
public IMedia GetParent(IMedia media)
|
|
{
|
|
if (media.ParentId == Constants.System.Root || media.ParentId == Constants.System.RecycleBinMedia)
|
|
return null;
|
|
|
|
return GetById(media.ParentId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IMedia"/> objects, which reside at the first level / root
|
|
/// </summary>
|
|
/// <returns>An Enumerable list of <see cref="IMedia"/> objects</returns>
|
|
public IEnumerable<IMedia> GetRootMedia()
|
|
{
|
|
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
|
{
|
|
scope.ReadLock(Constants.Locks.MediaTree);
|
|
var query = Query<IMedia>().Where(x => x.ParentId == Constants.System.Root);
|
|
return _mediaRepository.Get(query);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<IMedia> GetPagedMediaInRecycleBin(long pageIndex, int pageSize, out long totalRecords,
|
|
IQuery<IMedia> 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<IMedia>().Where(x => x.Path.StartsWith(Constants.System.RecycleBinMediaPathPrefix));
|
|
return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether an <see cref="IMedia"/> item has any children
|
|
/// </summary>
|
|
/// <param name="id">Id of the <see cref="IMedia"/></param>
|
|
/// <returns>True if the media has any children otherwise False</returns>
|
|
public bool HasChildren(int id)
|
|
{
|
|
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
|
{
|
|
var query = Query<IMedia>().Where(x => x.ParentId == id);
|
|
var count = _mediaRepository.Count(query);
|
|
return count > 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an <see cref="IMedia"/> object from the path stored in the 'umbracoFile' property.
|
|
/// </summary>
|
|
/// <param name="mediaPath">Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg)</param>
|
|
/// <returns><see cref="IMedia"/></returns>
|
|
public IMedia GetMediaByPath(string mediaPath)
|
|
{
|
|
using (ScopeProvider.CreateScope(autoComplete: true))
|
|
{
|
|
return _mediaRepository.GetMediaByPath(mediaPath);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Save
|
|
|
|
/// <summary>
|
|
/// Saves a single <see cref="IMedia"/> object
|
|
/// </summary>
|
|
/// <param name="media">The <see cref="IMedia"/> to save</param>
|
|
/// <param name="userId">Id of the User saving the Media</param>
|
|
/// <param name="raiseEvents">Optional boolean indicating whether or not to raise events.</param>
|
|
public Attempt<OperationResult> Save(IMedia media, int userId = 0, bool raiseEvents = true)
|
|
{
|
|
var evtMsgs = EventMessagesFactory.Get();
|
|
|
|
using (var scope = ScopeProvider.CreateScope())
|
|
{
|
|
var saveEventArgs = new SaveEventArgs<IMedia>(media, evtMsgs);
|
|
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs))
|
|
{
|
|
scope.Complete();
|
|
return OperationResult.Attempt.Cancel(evtMsgs);
|
|
}
|
|
|
|
// poor man's validation?
|
|
// poor man's validation?
|
|
|
|
if (string.IsNullOrWhiteSpace(media.Name))
|
|
throw new ArgumentException("Media has no name.", nameof(media));
|
|
|
|
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<IMedia>(media, changeType).ToEventArgs());
|
|
|
|
Audit(AuditType.Save, userId, media.Id);
|
|
scope.Complete();
|
|
}
|
|
|
|
return OperationResult.Attempt.Succeed(evtMsgs);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves a collection of <see cref="IMedia"/> objects
|
|
/// </summary>
|
|
/// <param name="medias">Collection of <see cref="IMedia"/> to save</param>
|
|
/// <param name="userId">Id of the User saving the Media</param>
|
|
/// <param name="raiseEvents">Optional boolean indicating whether or not to raise events.</param>
|
|
public Attempt<OperationResult> Save(IEnumerable<IMedia> medias, int userId = 0, bool raiseEvents = true)
|
|
{
|
|
var evtMsgs = EventMessagesFactory.Get();
|
|
var mediasA = medias.ToArray();
|
|
|
|
using (var scope = ScopeProvider.CreateScope())
|
|
{
|
|
var saveEventArgs = new SaveEventArgs<IMedia>(mediasA, evtMsgs);
|
|
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, new SaveEventArgs<IMedia>(mediasA, evtMsgs)))
|
|
{
|
|
scope.Complete();
|
|
return OperationResult.Attempt.Cancel(evtMsgs);
|
|
}
|
|
|
|
var treeChanges = mediasA.Select(x => new TreeChange<IMedia>(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
|
|
|
|
/// <summary>
|
|
/// Permanently deletes an <see cref="IMedia"/> object
|
|
/// </summary>
|
|
/// <param name="media">The <see cref="IMedia"/> to delete</param>
|
|
/// <param name="userId">Id of the User deleting the Media</param>
|
|
public Attempt<OperationResult> Delete(IMedia media, int userId = 0)
|
|
{
|
|
var evtMsgs = EventMessagesFactory.Get();
|
|
|
|
using (var scope = ScopeProvider.CreateScope())
|
|
{
|
|
if (scope.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs<IMedia>(media, evtMsgs)))
|
|
{
|
|
scope.Complete();
|
|
return OperationResult.Attempt.Cancel(evtMsgs);
|
|
}
|
|
|
|
scope.WriteLock(Constants.Locks.MediaTree);
|
|
|
|
DeleteLocked(scope, media);
|
|
|
|
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IMedia>(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<IMedia>(c, false); // raise event & get flagged files
|
|
scope.Events.Dispatch(Deleted, this, args);
|
|
|
|
_mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files
|
|
(file, e) => Logger.Error<MediaService>(e, "An error occurred while deleting file attached to nodes: {File}", file));
|
|
}
|
|
|
|
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.
|
|
|
|
/// <summary>
|
|
/// Permanently deletes versions from an <see cref="IMedia"/> object prior to a specific date.
|
|
/// This method will never delete the latest version of a media item.
|
|
/// </summary>
|
|
/// <param name="id">Id of the <see cref="IMedia"/> object to delete versions from</param>
|
|
/// <param name="versionDate">Latest version date</param>
|
|
/// <param name="userId">Optional Id of the User deleting versions of a Media object</param>
|
|
public void DeleteVersions(int id, DateTime versionDate, int userId = 0)
|
|
{
|
|
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<IMediaRepository>();
|
|
//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 = 0)
|
|
{
|
|
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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Permanently deletes specific version(s) from an <see cref="IMedia"/> object.
|
|
/// This method will never delete the latest version of a media item.
|
|
/// </summary>
|
|
/// <param name="id">Id of the <see cref="IMedia"/> object to delete a version from</param>
|
|
/// <param name="versionId">Id of the version to delete</param>
|
|
/// <param name="deletePriorVersions">Boolean indicating whether to delete versions prior to the versionId</param>
|
|
/// <param name="userId">Optional Id of the User deleting versions of a Media object</param>
|
|
public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = 0)
|
|
{
|
|
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
|
|
|
|
/// <summary>
|
|
/// Deletes an <see cref="IMedia"/> object by moving it to the Recycle Bin
|
|
/// </summary>
|
|
/// <param name="media">The <see cref="IMedia"/> to delete</param>
|
|
/// <param name="userId">Id of the User deleting the Media</param>
|
|
public Attempt<OperationResult> MoveToRecycleBin(IMedia media, int userId = 0)
|
|
{
|
|
var evtMsgs = EventMessagesFactory.Get();
|
|
var moves = new List<Tuple<IMedia, string>>();
|
|
|
|
using (var scope = ScopeProvider.CreateScope())
|
|
{
|
|
scope.WriteLock(Constants.Locks.MediaTree);
|
|
|
|
// fixme - missing 7.6 "ensure valid path" thing here?
|
|
// but then should be in PerformMoveLocked on every moved item?
|
|
|
|
var originalPath = media.Path;
|
|
|
|
if (scope.Events.DispatchCancelable(Trashing, this, new MoveEventArgs<IMedia>(new MoveEventInfo<IMedia>(media, originalPath, Constants.System.RecycleBinMedia)), 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<IMedia>(media, TreeChangeTypes.RefreshBranch).ToEventArgs());
|
|
var moveInfo = moves.Select(x => new MoveEventInfo<IMedia>(x.Item1, x.Item2, x.Item1.ParentId))
|
|
.ToArray();
|
|
|
|
scope.Events.Dispatch(Trashed, this, new MoveEventArgs<IMedia>(false, evtMsgs, moveInfo), nameof(Trashed));
|
|
Audit(AuditType.Move, userId, media.Id, "Move Media to recycle bin");
|
|
|
|
scope.Complete();
|
|
}
|
|
|
|
return OperationResult.Attempt.Succeed(evtMsgs);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves an <see cref="IMedia"/> object to a new location
|
|
/// </summary>
|
|
/// <param name="media">The <see cref="IMedia"/> to move</param>
|
|
/// <param name="parentId">Id of the Media's new Parent</param>
|
|
/// <param name="userId">Id of the User moving the Media</param>
|
|
public void Move(IMedia media, int parentId, int userId = 0)
|
|
{
|
|
// if moving to the recycle bin then use the proper method
|
|
if (parentId == Constants.System.RecycleBinMedia)
|
|
{
|
|
MoveToRecycleBin(media, userId);
|
|
return;
|
|
}
|
|
|
|
var moves = new List<Tuple<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 // causes rollback
|
|
|
|
var moveEventInfo = new MoveEventInfo<IMedia>(media, media.Path, parentId);
|
|
var moveEventArgs = new MoveEventArgs<IMedia>(moveEventInfo);
|
|
if (scope.Events.DispatchCancelable(Moving, this, moveEventArgs, nameof(Moving)))
|
|
{
|
|
scope.Complete();
|
|
return;
|
|
}
|
|
|
|
// 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
|
|
// 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<IMedia>(media, TreeChangeTypes.RefreshBranch).ToEventArgs());
|
|
var moveInfo = moves //changes
|
|
.Select(x => new MoveEventInfo<IMedia>(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();
|
|
}
|
|
}
|
|
|
|
// 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<Tuple<IMedia, string>> moves, bool? trash)
|
|
{
|
|
media.ParentId = parentId;
|
|
|
|
// get the level delta (old pos to new pos)
|
|
var levelDelta = parent == null
|
|
? 1 - media.Level + (parentId == Constants.System.RecycleBinMedia ? 1 : 0)
|
|
: parent.Level + 1 - media.Level;
|
|
|
|
var paths = new Dictionary<int, string>();
|
|
|
|
moves.Add(Tuple.Create(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" : "-1") : parent.Path) + "," + media.Id;
|
|
|
|
const int pageSize = 500;
|
|
var page = 0;
|
|
var total = long.MaxValue;
|
|
while (page * pageSize < total)
|
|
{
|
|
var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
|
|
foreach (var descendant in descendants)
|
|
{
|
|
moves.Add(Tuple.Create(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);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void PerformMoveMediaLocked(IMedia media, int userId, bool? trash)
|
|
{
|
|
if (trash.HasValue) ((ContentBase) media).Trashed = trash.Value;
|
|
_mediaRepository.Save(media);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Empties the Recycle Bin by deleting all <see cref="IMedia"/> that resides in the bin
|
|
/// </summary>
|
|
public OperationResult EmptyRecycleBin()
|
|
{
|
|
var nodeObjectType = Constants.ObjectTypes.Media;
|
|
var deleted = new List<IMedia>();
|
|
var evtMsgs = EventMessagesFactory.Get(); // todo - and then?
|
|
|
|
using (var scope = ScopeProvider.CreateScope())
|
|
{
|
|
scope.WriteLock(Constants.Locks.MediaTree);
|
|
|
|
// 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.
|
|
|
|
// 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
|
|
|
|
// emptying the recycle bin means deleting whetever is in there - do it properly!
|
|
// are managed by Delete, and not here.
|
|
// no idea what those events are for, keep a simplified version
|
|
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 whetever is in there - do it properly!
|
|
var query = Query<IMedia>().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<IMedia>(x, TreeChangeTypes.Remove)).ToEventArgs());
|
|
Audit(AuditType.Delete, 0, Constants.System.RecycleBinMedia, "Empty Media recycle bin");
|
|
scope.Complete();
|
|
}
|
|
|
|
return OperationResult.Succeed(evtMsgs);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Others
|
|
|
|
/// <summary>
|
|
/// Sorts a collection of <see cref="IMedia"/> objects by updating the SortOrder according
|
|
/// to the ordering of items in the passed in <see cref="IEnumerable{T}"/>.
|
|
/// </summary>
|
|
/// <param name="items"></param>
|
|
/// <param name="userId"></param>
|
|
/// <param name="raiseEvents"></param>
|
|
/// <returns>True if sorting succeeded, otherwise False</returns>
|
|
public bool Sort(IEnumerable<IMedia> items, int userId = 0, bool raiseEvents = true)
|
|
{
|
|
var itemsA = items.ToArray();
|
|
if (itemsA.Length == 0) return true;
|
|
|
|
using (var scope = ScopeProvider.CreateScope())
|
|
{
|
|
var args = new SaveEventArgs<IMedia>(itemsA);
|
|
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, args))
|
|
{
|
|
scope.Complete();
|
|
return false;
|
|
}
|
|
|
|
var saved = new List<IMedia>();
|
|
|
|
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 the current sort order equals that of the media we don't
|
|
|
|
// else update
|
|
// need to update it, so just increment the sort order and continue.
|
|
// save
|
|
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<IMedia>(x, TreeChangeTypes.RefreshNode)).ToEventArgs());
|
|
Audit(AuditType.Sort, userId, 0);
|
|
|
|
scope.Complete();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#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
|
|
|
|
/// <summary>
|
|
/// Occurs before Delete
|
|
/// </summary>
|
|
public static event TypedEventHandler<IMediaService, DeleteEventArgs<IMedia>> Deleting;
|
|
|
|
/// <summary>
|
|
/// Occurs after Delete
|
|
/// </summary>
|
|
public static event TypedEventHandler<IMediaService, DeleteEventArgs<IMedia>> Deleted;
|
|
|
|
/// <summary>
|
|
/// Occurs before Delete Versions
|
|
/// </summary>
|
|
public static event TypedEventHandler<IMediaService, DeleteRevisionsEventArgs> DeletingVersions;
|
|
|
|
/// <summary>
|
|
/// Occurs after Delete Versions
|
|
/// </summary>
|
|
public static event TypedEventHandler<IMediaService, DeleteRevisionsEventArgs> DeletedVersions;
|
|
|
|
/// <summary>
|
|
/// Occurs before Save
|
|
/// </summary>
|
|
public static event TypedEventHandler<IMediaService, SaveEventArgs<IMedia>> Saving;
|
|
|
|
/// <summary>
|
|
/// Occurs after Save
|
|
/// </summary>
|
|
public static event TypedEventHandler<IMediaService, SaveEventArgs<IMedia>> Saved;
|
|
|
|
/// <summary>
|
|
/// Occurs after Create
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Please note that the Media object has been created, but might not have been saved
|
|
/// so it does not have an identity yet (meaning no Id has been set).
|
|
/// </remarks>
|
|
public static event TypedEventHandler<IMediaService, NewEventArgs<IMedia>> Created;
|
|
|
|
/// <summary>
|
|
/// Occurs before Media is moved to Recycle Bin
|
|
/// </summary>
|
|
public static event TypedEventHandler<IMediaService, MoveEventArgs<IMedia>> Trashing;
|
|
|
|
/// <summary>
|
|
/// Occurs after Media is moved to Recycle Bin
|
|
/// </summary>
|
|
public static event TypedEventHandler<IMediaService, MoveEventArgs<IMedia>> Trashed;
|
|
|
|
/// <summary>
|
|
/// Occurs before Move
|
|
/// </summary>
|
|
public static event TypedEventHandler<IMediaService, MoveEventArgs<IMedia>> Moving;
|
|
|
|
/// <summary>
|
|
/// Occurs after Move
|
|
/// </summary>
|
|
public static event TypedEventHandler<IMediaService, MoveEventArgs<IMedia>> Moved;
|
|
|
|
/// <summary>
|
|
/// Occurs before the Recycle Bin is emptied
|
|
/// </summary>
|
|
public static event TypedEventHandler<IMediaService, RecycleBinEventArgs> EmptyingRecycleBin;
|
|
|
|
/// <summary>
|
|
/// Occurs after the Recycle Bin has been Emptied
|
|
/// </summary>
|
|
public static event TypedEventHandler<IMediaService, RecycleBinEventArgs> EmptiedRecycleBin;
|
|
|
|
/// <summary>
|
|
/// Occurs after change.
|
|
/// </summary>
|
|
internal static event TypedEventHandler<IMediaService, TreeChange<IMedia>.EventArgs> TreeChanged;
|
|
|
|
#endregion
|
|
|
|
#region Content Types
|
|
|
|
/// <summary>
|
|
/// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>This needs extra care and attention as its potentially a dangerous and extensive operation.</para>
|
|
/// <para>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.</para>
|
|
/// </remarks>
|
|
/// <param name="mediaTypeIds">Id of the <see cref="IMediaType"/></param>
|
|
/// <param name="userId">Optional id of the user deleting the media</param>
|
|
public void DeleteMediaOfTypes(IEnumerable<int> mediaTypeIds, int userId = 0)
|
|
{
|
|
//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<TreeChange<IMedia>>();
|
|
var moves = new List<Tuple<IMedia, string>>();
|
|
var mediaTypeIdsA = mediaTypeIds.ToArray();
|
|
|
|
using (var scope = ScopeProvider.CreateScope())
|
|
{
|
|
scope.WriteLock(Constants.Locks.MediaTree);
|
|
|
|
var query = Query<IMedia>().WhereIn(x => x.ContentTypeId, mediaTypeIdsA);
|
|
var medias = _mediaRepository.Get(query).ToArray();
|
|
|
|
if (scope.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs<IMedia>(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<IMedia>().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<IMedia>(media, TreeChangeTypes.RefreshBranch));
|
|
}
|
|
|
|
// delete media
|
|
// triggers the deleted event (and handles the files)
|
|
DeleteLocked(scope, media);
|
|
changes.Add(new TreeChange<IMedia>(media, TreeChangeTypes.Remove));
|
|
}
|
|
|
|
var moveInfos = moves.Select(x => new MoveEventInfo<IMedia>(x.Item1, x.Item2, x.Item1.ParentId))
|
|
.ToArray();
|
|
if (moveInfos.Length > 0)
|
|
scope.Events.Dispatch(Trashed, this, new MoveEventArgs<IMedia>(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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin.
|
|
/// </summary>
|
|
/// <remarks>This needs extra care and attention as its potentially a dangerous and extensive operation</remarks>
|
|
/// <param name="mediaTypeId">Id of the <see cref="IMediaType"/></param>
|
|
/// <param name="userId">Optional id of the user deleting the media</param>
|
|
public void DeleteMediaOfType(int mediaTypeId, int userId = 0)
|
|
{
|
|
DeleteMediaOfTypes(new[] { mediaTypeId }, userId);
|
|
}
|
|
|
|
private IMediaType GetMediaType(string mediaTypeAlias)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(mediaTypeAlias)) throw new ArgumentNullOrEmptyException(nameof(mediaTypeAlias));
|
|
|
|
using (var scope = ScopeProvider.CreateScope())
|
|
{
|
|
scope.ReadLock(Constants.Locks.MediaTypes);
|
|
|
|
var query = Query<IMediaType>().Where(x => x.Alias == mediaTypeAlias);
|
|
var mediaType = _mediaTypeRepository.Get(query).FirstOrDefault();
|
|
|
|
if (mediaType == null)
|
|
throw new Exception($"No MediaType matching the passed in Alias: '{mediaTypeAlias}' was found"); // causes rollback // causes rollback
|
|
|
|
scope.Complete();
|
|
return mediaType;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|