2017-12-28 09:18:09 +01:00
using System ;
using System.Collections.Generic ;
2019-04-17 16:39:10 +02:00
using System.ComponentModel ;
2017-12-28 09:18:09 +01:00
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
{
/// <summary>
/// Represents the Media Service, which is an easy access to operations involving <see cref="IMedia"/>
/// </summary>
2018-07-09 12:04:11 +02:00
public class MediaService : ScopeRepositoryService , IMediaService
2017-12-28 09:18:09 +01:00
{
private readonly IMediaRepository _mediaRepository ;
private readonly IMediaTypeRepository _mediaTypeRepository ;
private readonly IAuditRepository _auditRepository ;
private readonly IEntityRepository _entityRepository ;
2018-10-26 15:06:53 +02:00
private readonly IMediaFileSystem _mediaFileSystem ;
2017-12-28 09:18:09 +01:00
#region Constructors
2018-10-26 15:06:53 +02:00
public MediaService ( IScopeProvider provider , IMediaFileSystem mediaFileSystem , ILogger logger , IEventMessagesFactory eventMessagesFactory ,
2017-12-28 09:18:09 +01:00
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>
2019-02-06 14:01:14 +00:00
public IMedia CreateMedia ( string name , Guid parentId , string mediaTypeAlias , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
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>
2019-02-06 14:01:14 +00:00
public IMedia CreateMedia ( string name , int parentId , string mediaTypeAlias , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
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 ) ) ;
2019-08-29 16:32:53 +01:00
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." ) ;
}
2017-12-28 09:18:09 +01:00
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>
2019-02-06 14:01:14 +00:00
public IMedia CreateMedia ( string name , string mediaTypeAlias , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
// not locking since not saving anything
var mediaType = GetMediaType ( mediaTypeAlias ) ;
if ( mediaType = = null )
throw new ArgumentException ( "No media type with that alias." , nameof ( mediaTypeAlias ) ) ;
2019-08-29 16:32:53 +01:00
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." ) ;
}
2017-12-28 09:18:09 +01:00
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>
2019-02-06 14:01:14 +00:00
public IMedia CreateMedia ( string name , IMedia parent , string mediaTypeAlias , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
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 )
2019-06-23 12:12:46 +01:00
throw new ArgumentException ( "No media type with that alias." , nameof ( mediaTypeAlias ) ) ; // causes rollback
2019-08-29 16:32:53 +01:00
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." ) ;
}
2017-12-28 09:18:09 +01:00
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>
2019-02-06 14:01:14 +00:00
public IMedia CreateMediaWithIdentity ( string name , int parentId , string mediaTypeAlias , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
using ( var scope = ScopeProvider . CreateScope ( ) )
{
// locking the media tree secures media types too
scope . WriteLock ( Constants . Locks . MediaTree ) ;
2019-06-23 12:12:46 +01:00
var mediaType = GetMediaType ( mediaTypeAlias ) ; // + locks
2017-12-28 09:18:09 +01:00
if ( mediaType = = null )
2019-06-23 12:12:46 +01:00
throw new ArgumentException ( "No media type with that alias." , nameof ( mediaTypeAlias ) ) ; // causes rollback
2017-12-28 09:18:09 +01:00
2019-06-23 12:12:46 +01:00
var parent = parentId > 0 ? GetById ( parentId ) : null ; // + locks
2017-12-28 09:18:09 +01:00
if ( parentId > 0 & & parent = = null )
2019-06-23 12:12:46 +01:00
throw new ArgumentException ( "No media with that id." , nameof ( parentId ) ) ; // causes rollback
2017-12-28 09:18:09 +01:00
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>
2019-02-06 14:01:14 +00:00
public IMedia CreateMediaWithIdentity ( string name , IMedia parent , string mediaTypeAlias , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
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 ) ;
2019-06-23 12:12:46 +01:00
var mediaType = GetMediaType ( mediaTypeAlias ) ; // + locks
2017-12-28 09:18:09 +01:00
if ( mediaType = = null )
2019-06-23 12:12:46 +01:00
throw new ArgumentException ( "No media type with that alias." , nameof ( mediaTypeAlias ) ) ; // causes rollback
2017-12-28 09:18:09 +01:00
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 )
{
2018-01-10 12:48:51 +01:00
// if saving is cancelled, media remains without an identity
2017-12-28 09:18:09 +01:00
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 ( ) ) ;
}
2019-04-25 17:47:10 +02:00
2018-01-10 12:48:51 +01:00
if ( withIdentity = = false )
return ;
2017-12-28 09:18:09 +01:00
2018-10-18 22:47:12 +11:00
Audit ( AuditType . New , media . CreatorId , media . Id , $"Media '{media.Name}' was created with Id {media.Id}" ) ;
2017-12-28 09:18:09 +01:00
}
#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 ) ;
}
}
2018-11-01 01:01:50 +11:00
/// <inheritdoc />
2018-11-01 10:28:53 +11:00
public IEnumerable < IMedia > GetPagedOfType ( int contentTypeId , long pageIndex , int pageSize , out long totalRecords , IQuery < IMedia > filter = null , Ordering ordering = null )
2017-12-28 09:18:09 +01:00
{
2018-11-01 01:01:50 +11:00
if ( pageIndex < 0 ) throw new ArgumentOutOfRangeException ( nameof ( pageIndex ) ) ;
if ( pageSize < = 0 ) throw new ArgumentOutOfRangeException ( nameof ( pageSize ) ) ;
if ( ordering = = null )
ordering = Ordering . By ( "sortOrder" ) ;
2017-12-28 09:18:09 +01:00
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
2018-11-01 01:01:50 +11:00
scope . ReadLock ( Constants . Locks . ContentTree ) ;
return _mediaRepository . GetPage (
Query < IMedia > ( ) . Where ( x = > x . ContentTypeId = = contentTypeId ) ,
pageIndex , pageSize , out totalRecords , filter , ordering ) ;
}
}
/// <inheritdoc />
2018-11-01 10:28:53 +11:00
public IEnumerable < IMedia > GetPagedOfTypes ( int [ ] contentTypeIds , long pageIndex , int pageSize , out long totalRecords , IQuery < IMedia > filter = null , Ordering ordering = null )
2018-11-01 01:01:50 +11:00
{
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 ) ;
2017-12-28 09:18:09 +01:00
}
}
/// <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 )
{
2019-01-22 18:03:39 -05:00
// intentionally not locking
2017-12-28 09:18:09 +01:00
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 > ( ) ;
2018-03-22 17:41:13 +01:00
var rootId = Constants . System . RootString ;
2017-12-28 09:18:09 +01:00
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 ) ;
}
}
2018-11-01 10:28:53 +11:00
/// <inheritdoc />
2018-11-01 00:21:52 +11:00
public IEnumerable < IMedia > GetPagedChildren ( int id , long pageIndex , int pageSize , out long totalChildren ,
2018-11-01 10:28:53 +11:00
IQuery < IMedia > filter = null , Ordering ordering = null )
2017-12-28 09:18:09 +01:00
{
if ( pageIndex < 0 ) throw new ArgumentOutOfRangeException ( nameof ( pageIndex ) ) ;
if ( pageSize < = 0 ) throw new ArgumentOutOfRangeException ( nameof ( pageSize ) ) ;
2018-11-01 00:21:52 +11:00
if ( ordering = = null )
ordering = Ordering . By ( "sortOrder" ) ;
2017-12-28 09:18:09 +01:00
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . MediaTree ) ;
2018-11-01 00:21:52 +11:00
var query = Query < IMedia > ( ) . Where ( x = > x . ParentId = = id ) ;
return _mediaRepository . GetPage ( query , pageIndex , pageSize , out totalChildren , filter , ordering ) ;
2017-12-28 09:18:09 +01:00
}
}
2018-11-01 10:28:53 +11:00
/// <inheritdoc />
2018-11-01 00:21:52 +11:00
public IEnumerable < IMedia > GetPagedDescendants ( int id , long pageIndex , int pageSize , out long totalChildren ,
2018-11-01 10:28:53 +11:00
IQuery < IMedia > filter = null , Ordering ordering = null )
2017-12-28 09:18:09 +01:00
{
2018-11-01 00:21:52 +11:00
if ( ordering = = null )
ordering = Ordering . By ( "Path" ) ;
2017-12-28 09:18:09 +01:00
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 > ( ) ;
}
2020-04-07 13:29:00 +10:00
return GetPagedLocked ( GetPagedDescendantQuery ( mediaPath [ 0 ] . Path ) , pageIndex , pageSize , out totalChildren , filter , ordering ) ;
2017-12-28 09:18:09 +01:00
}
2020-04-07 13:29:00 +10:00
return GetPagedLocked ( GetPagedDescendantQuery ( null ) , pageIndex , pageSize , out totalChildren , filter , ordering ) ;
2017-12-28 09:18:09 +01:00
}
}
2020-04-07 13:29:00 +10:00
private IQuery < IMedia > GetPagedDescendantQuery ( string mediaPath )
{
var query = Query < IMedia > ( ) ;
if ( ! mediaPath . IsNullOrWhiteSpace ( ) )
query . Where ( x = > x . Path . SqlStartsWith ( mediaPath + "," , TextColumnType . NVarchar ) ) ;
return query ;
}
private IEnumerable < IMedia > GetPagedLocked ( IQuery < IMedia > query , long pageIndex , int pageSize , out long totalChildren ,
2018-11-01 00:21:52 +11:00
IQuery < IMedia > filter , Ordering ordering )
2017-12-28 09:18:09 +01:00
{
2018-10-31 23:11:37 +11:00
if ( pageIndex < 0 ) throw new ArgumentOutOfRangeException ( nameof ( pageIndex ) ) ;
if ( pageSize < = 0 ) throw new ArgumentOutOfRangeException ( nameof ( pageSize ) ) ;
2018-11-01 00:21:52 +11:00
if ( ordering = = null ) throw new ArgumentNullException ( nameof ( ordering ) ) ;
2017-12-28 09:18:09 +01:00
2018-11-01 00:21:52 +11:00
return _mediaRepository . GetPage ( query , pageIndex , pageSize , out totalChildren , filter , ordering ) ;
2017-12-28 09:18:09 +01:00
}
/// <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 )
{
2019-01-22 18:03:39 -05:00
// intentionally not locking
2017-12-28 09:18:09 +01:00
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 ) ;
}
}
2018-11-01 10:22:45 +11:00
/// <inheritdoc />
public IEnumerable < IMedia > GetPagedMediaInRecycleBin ( long pageIndex , int pageSize , out long totalRecords ,
IQuery < IMedia > filter = null , Ordering ordering = null )
2017-12-28 09:18:09 +01:00
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
2018-11-01 10:22:45 +11:00
if ( ordering = = null )
ordering = Ordering . By ( "Path" ) ;
2017-12-28 09:18:09 +01:00
scope . ReadLock ( Constants . Locks . MediaTree ) ;
2018-03-22 17:41:13 +01:00
var query = Query < IMedia > ( ) . Where ( x = > x . Path . StartsWith ( Constants . System . RecycleBinMediaPathPrefix ) ) ;
2018-11-01 10:22:45 +11:00
return _mediaRepository . GetPage ( query , pageIndex , pageSize , out totalRecords , filter , ordering ) ;
2017-12-28 09:18:09 +01:00
}
}
/// <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 )
{
2018-03-22 17:41:13 +01:00
using ( ScopeProvider . CreateScope ( autoComplete : true ) )
2017-12-28 09:18:09 +01:00
{
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>
2019-02-06 14:01:14 +00:00
public Attempt < OperationResult > Save ( IMedia media , int userId = Constants . Security . SuperUserId , bool raiseEvents = true )
2017-12-28 09:18:09 +01:00
{
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?
if ( string . IsNullOrWhiteSpace ( media . Name ) )
throw new ArgumentException ( "Media has no name." , nameof ( media ) ) ;
2019-08-29 16:32:53 +01:00
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." ) ;
}
2017-12-28 09:18:09 +01:00
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 ) ;
}
2018-06-22 21:03:47 +02:00
var changeType = TreeChangeTypes . RefreshNode ;
2017-12-28 09:18:09 +01:00
scope . Events . Dispatch ( TreeChanged , this , new TreeChange < IMedia > ( media , changeType ) . ToEventArgs ( ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Save , userId , media . Id ) ;
2017-12-28 09:18:09 +01:00
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>
2019-02-06 14:01:14 +00:00
public Attempt < OperationResult > Save ( IEnumerable < IMedia > medias , int userId = Constants . Security . SuperUserId , bool raiseEvents = true )
2017-12-28 09:18:09 +01:00
{
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 ) ;
}
2018-06-22 21:03:47 +02:00
var treeChanges = mediasA . Select ( x = > new TreeChange < IMedia > ( x , TreeChangeTypes . RefreshNode ) ) ;
2017-12-28 09:18:09 +01:00
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 ( ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Save , userId = = - 1 ? 0 : userId , Constants . System . Root , "Bulk save media" ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
return OperationResult . Attempt . Succeed ( evtMsgs ) ;
}
#endregion
#region Delete
2019-04-25 17:47:10 +02:00
2017-12-28 09:18:09 +01:00
/// <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>
2019-02-06 14:01:14 +00:00
public Attempt < OperationResult > Delete ( IMedia media , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
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 ( ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Delete , userId , media . Id ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
return OperationResult . Attempt . Succeed ( evtMsgs ) ;
}
private void DeleteLocked ( IScope scope , IMedia media )
{
2018-10-31 23:11:37 +11:00
void DoDelete ( IMedia c )
2017-12-28 09:18:09 +01:00
{
_mediaRepository . Delete ( c ) ;
var args = new DeleteEventArgs < IMedia > ( c , false ) ; // raise event & get flagged files
scope . Events . Dispatch ( Deleted , this , args ) ;
2018-10-26 15:06:53 +02:00
// media files deleted by QueuingEventDispatcher
2017-12-28 09:18:09 +01:00
}
2018-10-31 23:11:37 +11:00
const int pageSize = 500 ;
var page = 0 ;
var total = long . MaxValue ;
2019-08-29 16:32:53 +01:00
while ( page * pageSize < total )
2018-10-31 23:11:37 +11:00
{
//get descendants - ordered from deepest to shallowest
2018-11-01 00:21:52 +11:00
var descendants = GetPagedDescendants ( media . Id , page , pageSize , out total , ordering : Ordering . By ( "Path" , Direction . Descending ) ) ;
2018-10-31 23:11:37 +11:00
foreach ( var c in descendants )
DoDelete ( c ) ;
}
DoDelete ( media ) ;
2017-12-28 09:18:09 +01:00
}
2019-01-27 01:17:32 -05:00
//TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way
2017-12-28 09:18:09 +01:00
// 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>
2019-02-06 14:01:14 +00:00
public void DeleteVersions ( int id , DateTime versionDate , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
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));
2018-10-18 22:47:12 +11:00
//Audit(uow, AuditType.Delete, "Delete Media by version date, userId, Constants.System.Root);
2017-12-28 09:18:09 +01:00
//uow.Complete();
}
}
2019-02-06 14:01:14 +00:00
private void DeleteVersions ( IScope scope , bool wlock , int id , DateTime versionDate , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
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 ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Delete , userId , Constants . System . Root , "Delete Media by version date" ) ;
2017-12-28 09:18:09 +01:00
}
/// <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>
2019-02-06 14:01:14 +00:00
public void DeleteVersion ( int id , int versionId , bool deletePriorVersions , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
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 ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Delete , userId , Constants . System . Root , "Delete Media by version" ) ;
2017-12-28 09:18:09 +01:00
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>
2019-02-06 14:01:14 +00:00
public Attempt < OperationResult > MoveToRecycleBin ( IMedia media , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
var evtMsgs = EventMessagesFactory . Get ( ) ;
2020-04-07 13:29:00 +10:00
var moves = new List < ( IMedia , string ) > ( ) ;
2017-12-28 09:18:09 +01:00
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . MediaTree ) ;
2019-01-26 09:42:14 -05:00
// TODO: missing 7.6 "ensure valid path" thing here?
2017-12-28 09:18:09 +01:00
// but then should be in PerformMoveLocked on every moved item?
var originalPath = media . Path ;
2018-11-20 14:11:35 +11:00
var moveEventInfo = new MoveEventInfo < IMedia > ( media , originalPath , Constants . System . RecycleBinMedia ) ;
var moveEventArgs = new MoveEventArgs < IMedia > ( true , evtMsgs , moveEventInfo ) ;
2018-11-19 17:54:36 +11:00
if ( scope . Events . DispatchCancelable ( Trashing , this , moveEventArgs , nameof ( Trashing ) ) )
2017-12-28 09:18:09 +01:00
{
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 ( ) ;
2018-11-19 17:54:36 +11:00
moveEventArgs . MoveInfoCollection = moveInfo ;
moveEventArgs . CanCancel = false ;
scope . Events . Dispatch ( Trashed , this , moveEventArgs , nameof ( Trashed ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Move , userId , media . Id , "Move Media to recycle bin" ) ;
2017-12-28 09:18:09 +01:00
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>
2019-02-06 14:01:14 +00:00
public Attempt < OperationResult > Move ( IMedia media , int parentId , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
2018-11-19 17:54:36 +11:00
var evtMsgs = EventMessagesFactory . Get ( ) ;
2017-12-28 09:18:09 +01:00
// if moving to the recycle bin then use the proper method
if ( parentId = = Constants . System . RecycleBinMedia )
{
2018-07-09 12:04:11 +02:00
MoveToRecycleBin ( media , userId ) ;
2018-11-19 17:54:36 +11:00
return OperationResult . Attempt . Succeed ( evtMsgs ) ;
2017-12-28 09:18:09 +01:00
}
2020-04-07 13:29:00 +10:00
var moves = new List < ( IMedia , string ) > ( ) ;
2017-12-28 09:18:09 +01:00
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 ) )
2019-06-23 12:12:46 +01:00
throw new InvalidOperationException ( "Parent does not exist or is trashed." ) ; // causes rollback
2017-12-28 09:18:09 +01:00
var moveEventInfo = new MoveEventInfo < IMedia > ( media , media . Path , parentId ) ;
2018-11-19 17:54:36 +11:00
var moveEventArgs = new MoveEventArgs < IMedia > ( true , evtMsgs , moveEventInfo ) ;
2018-08-08 11:49:05 +10:00
if ( scope . Events . DispatchCancelable ( Moving , this , moveEventArgs , nameof ( Moving ) ) )
2017-12-28 09:18:09 +01:00
{
scope . Complete ( ) ;
2018-11-19 17:54:36 +11:00
return OperationResult . Attempt . Cancel ( evtMsgs ) ;
2017-12-28 09:18:09 +01:00
}
// 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
2019-08-29 16:32:53 +01:00
var trashed = media . Trashed ? false : ( bool? ) null ;
2017-12-28 09:18:09 +01:00
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 ;
2018-08-08 11:49:05 +10:00
scope . Events . Dispatch ( Moved , this , moveEventArgs , nameof ( Moved ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Move , userId , media . Id ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
2018-11-19 17:54:36 +11:00
return OperationResult . Attempt . Succeed ( evtMsgs ) ;
2017-12-28 09:18:09 +01:00
}
// MUST be called from within WriteLock
// trash indicates whether we are trashing, un-trashing, or not changing anything
2020-04-07 13:29:00 +10:00
private void PerformMoveLocked ( IMedia media , int parentId , IMedia parent , int userId , ICollection < ( IMedia , string ) > moves , bool? trash )
2017-12-28 09:18:09 +01:00
{
media . ParentId = parentId ;
// get the level delta (old pos to new pos)
2019-04-25 17:47:10 +02:00
// note that recycle bin (id:-20) level is 0!
var levelDelta = 1 - media . Level + ( parent ? . Level ? ? 0 ) ;
2017-12-28 09:18:09 +01:00
var paths = new Dictionary < int , string > ( ) ;
2020-04-07 13:29:00 +10:00
moves . Add ( ( media , media . Path ) ) ; // capture original path
2017-12-28 09:18:09 +01:00
2018-10-31 23:11:37 +11:00
//need to store the original path to lookup descendants based on it below
var originalPath = media . Path ;
2017-12-28 09:18:09 +01:00
// 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;
2019-03-29 12:18:52 +00:00
paths [ media . Id ] = ( parent = = null ? ( parentId = = Constants . System . RecycleBinMedia ? "-1,-21" : Constants . System . RootString ) : parent . Path ) + "," + media . Id ;
2017-12-28 09:18:09 +01:00
2018-10-31 23:11:37 +11:00
const int pageSize = 500 ;
2020-04-07 13:29:00 +10:00
var query = GetPagedDescendantQuery ( originalPath ) ;
long total ;
do
2017-12-28 09:18:09 +01:00
{
2020-04-07 15:02:08 +10:00
// We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced
2020-04-07 13:29:00 +10:00
var descendants = GetPagedLocked ( query , 0 , pageSize , out total , null , Ordering . By ( "Path" , Direction . Ascending ) ) ;
2018-10-31 23:11:37 +11:00
foreach ( var descendant in descendants )
{
2020-04-07 13:29:00 +10:00
moves . Add ( ( descendant , descendant . Path ) ) ; // capture original path
2017-12-28 09:18:09 +01:00
2018-10-31 23:11:37 +11:00
// 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 ) ;
}
2020-04-07 13:29:00 +10:00
} while ( total > pageSize ) ;
2017-12-28 09:18:09 +01:00
}
private void PerformMoveMediaLocked ( IMedia media , int userId , bool? trash )
{
2019-08-29 16:32:53 +01:00
if ( trash . HasValue ) ( ( ContentBase ) media ) . Trashed = trash . Value ;
2017-12-28 09:18:09 +01:00
_mediaRepository . Save ( media ) ;
}
/// <summary>
/// Empties the Recycle Bin by deleting all <see cref="IMedia"/> that resides in the bin
/// </summary>
2019-04-17 16:39:10 +02:00
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")]
public OperationResult EmptyRecycleBin ( ) = > EmptyRecycleBin ( Constants . Security . SuperUserId ) ;
/// <summary>
/// Empties the Recycle Bin by deleting all <see cref="IMedia"/> that resides in the bin
/// </summary>
2019-04-25 17:47:10 +02:00
/// <param name="userId">Optional Id of the User emptying the Recycle Bin</param>
2019-04-17 16:39:10 +02:00
public OperationResult EmptyRecycleBin ( int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
var nodeObjectType = Constants . ObjectTypes . Media ;
var deleted = new List < IMedia > ( ) ;
2019-01-26 09:42:14 -05:00
var evtMsgs = EventMessagesFactory . Get ( ) ; // TODO: and then?
2017-12-28 09:18:09 +01:00
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . MediaTree ) ;
// no idea what those events are for, keep a simplified version
2019-06-23 12:12:46 +01:00
2017-12-28 09:18:09 +01:00
// 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.
2018-04-04 13:11:12 +10:00
var args = new RecycleBinEventArgs ( nodeObjectType , evtMsgs ) ;
2017-12-28 09:18:09 +01:00
if ( scope . Events . DispatchCancelable ( EmptyingRecycleBin , this , args ) )
{
scope . Complete ( ) ;
2018-04-04 13:11:12 +10:00
return OperationResult . Cancel ( evtMsgs ) ;
2017-12-28 09:18:09 +01:00
}
2019-01-22 18:03:39 -05:00
// emptying the recycle bin means deleting whatever is in there - do it properly!
2017-12-28 09:18:09 +01:00
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 ( ) ) ;
2019-04-17 16:39:10 +02:00
Audit ( AuditType . Delete , userId , Constants . System . RecycleBinMedia , "Empty Media recycle bin" ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
2018-04-04 13:11:12 +10:00
return OperationResult . Succeed ( evtMsgs ) ;
2017-12-28 09:18:09 +01:00
}
#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>
2019-02-06 14:01:14 +00:00
public bool Sort ( IEnumerable < IMedia > items , int userId = Constants . Security . SuperUserId , bool raiseEvents = true )
2017-12-28 09:18:09 +01:00
{
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 ( 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 ( ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Sort , userId , 0 ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
return true ;
2020-04-07 01:02:08 +10:00
}
2020-04-07 16:42:21 +10:00
public ContentDataIntegrityReport CheckDataIntegrity ( ContentDataIntegrityReportOptions options )
2020-04-07 01:02:08 +10:00
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . WriteLock ( Constants . Locks . MediaTree ) ;
2020-04-08 15:03:21 +10:00
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 < IMedia > . EventArgs ( new TreeChange < IMedia > ( root , TreeChangeTypes . RefreshAll ) ) ) ;
}
return report ;
2020-04-07 01:02:08 +10:00
}
2017-12-28 09:18:09 +01:00
}
#endregion
#region Private Methods
2018-10-18 22:47:12 +11:00
private void Audit ( AuditType type , int userId , int objectId , string message = null )
2017-12-28 09:18:09 +01:00
{
2018-10-18 22:47:12 +11:00
_auditRepository . Save ( new AuditItem ( objectId , type , userId , ObjectTypes . GetName ( UmbracoObjectTypes . Media ) , message ) ) ;
2017-12-28 09:18:09 +01:00
}
#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 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>
2018-08-08 11:49:05 +10:00
/// <param name="mediaTypeIds">Id of the <see cref="IMediaType"/></param>
2017-12-28 09:18:09 +01:00
/// <param name="userId">Optional id of the user deleting the media</param>
2019-02-06 14:01:14 +00:00
public void DeleteMediaOfTypes ( IEnumerable < int > mediaTypeIds , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
2019-01-27 01:17:32 -05:00
// TODO: This currently this is called from the ContentTypeService but that needs to change,
2017-12-28 09:18:09 +01:00
// 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 > > ( ) ;
2020-04-07 13:29:00 +10:00
var moves = new List < ( IMedia , string ) > ( ) ;
2017-12-28 09:18:09 +01:00
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 )
2018-08-08 11:49:05 +10:00
scope . Events . Dispatch ( Trashed , this , new MoveEventArgs < IMedia > ( false , moveInfos ) , nameof ( Trashed ) ) ;
2017-12-28 09:18:09 +01:00
scope . Events . Dispatch ( TreeChanged , this , changes . ToEventArgs ( ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Delete , userId , Constants . System . Root , $"Delete Media of types {string.Join(" , ", mediaTypeIdsA)}" ) ;
2017-12-28 09:18:09 +01:00
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>
2019-02-06 14:01:14 +00:00
public void DeleteMediaOfType ( int mediaTypeId , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
DeleteMediaOfTypes ( new [ ] { mediaTypeId } , userId ) ;
}
private IMediaType GetMediaType ( string mediaTypeAlias )
{
2019-10-07 22:10:21 +02:00
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 ) ) ;
2017-12-28 09:18:09 +01:00
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 )
2019-10-07 22:10:21 +02:00
throw new InvalidOperationException ( $"No media type matched the specified alias '{mediaTypeAlias}'." ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
return mediaType ;
}
}
#endregion
2020-04-07 01:02:08 +10:00
2017-12-28 09:18:09 +01:00
}
}