2021-09-30 22:45:09 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
using Humanizer ;
using osu.Framework.Extensions ;
using osu.Framework.Extensions.IEnumerableExtensions ;
using osu.Framework.Logging ;
using osu.Framework.Platform ;
using osu.Framework.Threading ;
2021-11-19 15:07:55 +08:00
using osu.Game.Extensions ;
2021-09-30 22:45:09 +08:00
using osu.Game.IO.Archives ;
using osu.Game.Models ;
using osu.Game.Overlays.Notifications ;
using Realms ;
2022-06-15 16:13:32 +08:00
namespace osu.Game.Database
2021-09-30 22:45:09 +08:00
{
/// <summary>
/// Encapsulates a model store class to give it import functionality.
/// Adds cross-functionality with <see cref="RealmFileStore"/> to give access to the central file store for the provided model.
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
2021-10-15 16:08:43 +08:00
public abstract class RealmArchiveModelImporter < TModel > : IModelImporter < TModel >
2021-09-30 22:45:09 +08:00
where TModel : RealmObject , IHasRealmFiles , IHasGuidPrimaryKey , ISoftDelete
{
2022-06-15 14:31:58 +08:00
/// <summary>
/// The maximum number of concurrent imports to run per import scheduler.
/// </summary>
2021-09-30 22:45:09 +08:00
private const int import_queue_request_concurrency = 1 ;
2022-06-15 14:31:58 +08:00
/// <summary>
/// The minimum number of items in a single import call in order for the import to be processed as a batch.
/// Batch imports will apply optimisations preferring speed over consistency when detecting changes in already-imported items.
/// </summary>
private const int minimum_items_considered_batch_import = 10 ;
2021-09-30 22:45:09 +08:00
/// <summary>
2021-10-15 16:08:43 +08:00
/// A singleton scheduler shared by all <see cref="RealmArchiveModelImporter{TModel}"/>.
2021-09-30 22:45:09 +08:00
/// </summary>
/// <remarks>
/// This scheduler generally performs IO and CPU intensive work so concurrency is limited harshly.
/// It is mainly being used as a queue mechanism for large imports.
/// </remarks>
2021-10-15 16:08:43 +08:00
private static readonly ThreadedTaskScheduler import_scheduler = new ThreadedTaskScheduler ( import_queue_request_concurrency , nameof ( RealmArchiveModelImporter < TModel > ) ) ;
2021-09-30 22:45:09 +08:00
/// <summary>
2022-06-14 23:46:00 +08:00
/// A second scheduler for batch imports.
2021-09-30 22:45:09 +08:00
/// For simplicity, these will just run in parallel with normal priority imports, but a future refactor would see this implemented via a custom scheduler/queue.
/// See https://gist.github.com/peppy/f0e118a14751fc832ca30dd48ba3876b for an incomplete version of this.
/// </summary>
2022-06-14 23:46:00 +08:00
private static readonly ThreadedTaskScheduler import_scheduler_batch = new ThreadedTaskScheduler ( import_queue_request_concurrency , nameof ( RealmArchiveModelImporter < TModel > ) ) ;
2021-09-30 22:45:09 +08:00
2023-01-09 17:54:11 +08:00
/// <summary>
/// Temporarily pause imports to avoid performance overheads affecting gameplay scenarios.
/// </summary>
2023-01-10 00:10:20 +08:00
public bool PauseImports { get ; set ; }
2023-01-09 17:54:11 +08:00
2022-06-20 17:38:02 +08:00
public abstract IEnumerable < string > HandledExtensions { get ; }
2021-09-30 22:45:09 +08:00
protected readonly RealmFileStore Files ;
2022-01-25 11:58:15 +08:00
protected readonly RealmAccess Realm ;
2021-09-30 22:45:09 +08:00
/// <summary>
/// Fired when the user requests to view the resulting import.
/// </summary>
2022-06-20 17:21:37 +08:00
public Action < IEnumerable < Live < TModel > > > ? PresentImport { get ; set ; }
2021-09-30 22:45:09 +08:00
/// <summary>
/// Set an endpoint for notifications to be posted to.
/// </summary>
2022-06-16 17:07:04 +08:00
public Action < Notification > ? PostNotification { get ; set ; }
2021-09-30 22:45:09 +08:00
2022-01-24 18:59:58 +08:00
protected RealmArchiveModelImporter ( Storage storage , RealmAccess realm )
2021-09-30 22:45:09 +08:00
{
2022-01-25 11:58:15 +08:00
Realm = realm ;
2021-09-30 22:45:09 +08:00
2022-01-24 18:59:58 +08:00
Files = new RealmFileStore ( realm , storage ) ;
2021-09-30 22:45:09 +08:00
}
2022-06-14 19:01:11 +08:00
public Task Import ( params string [ ] paths ) = > Import ( paths . Select ( p = > new ImportTask ( p ) ) . ToArray ( ) ) ;
2021-09-30 22:45:09 +08:00
2022-12-13 20:03:25 +08:00
public Task Import ( ImportTask [ ] tasks , ImportParameters parameters = default )
2021-09-30 22:45:09 +08:00
{
var notification = new ProgressNotification { State = ProgressNotificationState . Active } ;
PostNotification ? . Invoke ( notification ) ;
2022-12-13 20:03:25 +08:00
return Import ( notification , tasks , parameters ) ;
2021-09-30 22:45:09 +08:00
}
2022-12-13 20:03:25 +08:00
public async Task < IEnumerable < Live < TModel > > > Import ( ProgressNotification notification , ImportTask [ ] tasks , ImportParameters parameters = default )
2021-09-30 22:45:09 +08:00
{
if ( tasks . Length = = 0 )
{
notification . CompletionText = $"No {HumanisedModelName}s were found to import!" ;
notification . State = ProgressNotificationState . Completed ;
return Enumerable . Empty < RealmLive < TModel > > ( ) ;
}
notification . Progress = 0 ;
notification . Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising..." ;
int current = 0 ;
2022-01-26 12:37:33 +08:00
var imported = new List < Live < TModel > > ( ) ;
2021-09-30 22:45:09 +08:00
2022-12-13 20:03:25 +08:00
parameters . Batch | = tasks . Length > = minimum_items_considered_batch_import ;
2021-09-30 22:45:09 +08:00
2022-09-05 10:28:12 +08:00
await Task . WhenAll ( tasks . Select ( async task = >
2021-09-30 22:45:09 +08:00
{
2022-09-05 10:28:12 +08:00
if ( notification . CancellationToken . IsCancellationRequested )
return ;
try
2021-09-30 22:45:09 +08:00
{
2022-12-13 20:03:25 +08:00
var model = await Import ( task , parameters , notification . CancellationToken ) . ConfigureAwait ( false ) ;
2021-09-30 22:45:09 +08:00
2022-09-05 10:28:12 +08:00
lock ( imported )
2021-09-30 22:45:09 +08:00
{
2022-09-05 10:28:12 +08:00
if ( model ! = null )
imported . Add ( model ) ;
current + + ;
2021-09-30 22:45:09 +08:00
2022-09-05 10:28:12 +08:00
notification . Text = $"Imported {current} of {tasks.Length} {HumanisedModelName}s" ;
notification . Progress = ( float ) current / tasks . Length ;
2021-09-30 22:45:09 +08:00
}
2022-09-05 10:28:12 +08:00
}
catch ( OperationCanceledException )
{
}
catch ( Exception e )
{
Logger . Error ( e , $@"Could not import ({task})" , LoggingTarget . Database ) ;
}
} ) ) . ConfigureAwait ( false ) ;
if ( imported . Count = = 0 )
2021-09-30 22:45:09 +08:00
{
2022-09-05 10:28:12 +08:00
if ( notification . CancellationToken . IsCancellationRequested )
2021-09-30 22:45:09 +08:00
{
notification . State = ProgressNotificationState . Cancelled ;
return imported ;
}
2023-09-26 14:00:56 +08:00
notification . Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import failed! Check logs for more information." ;
2021-09-30 22:45:09 +08:00
notification . State = ProgressNotificationState . Cancelled ;
}
else
{
2023-01-10 18:32:03 +08:00
if ( tasks . Length > imported . Count )
2023-01-10 21:27:36 +08:00
notification . CompletionText = $"Imported {imported.Count} of {tasks.Length} {HumanisedModelName}s." ;
2023-01-10 18:32:03 +08:00
else if ( imported . Count > 1 )
notification . CompletionText = $"Imported {imported.Count} {HumanisedModelName}s!" ;
else
notification . CompletionText = $"Imported {imported.First().GetDisplayString()}!" ;
2021-09-30 22:45:09 +08:00
2022-06-20 17:21:37 +08:00
if ( imported . Count > 0 & & PresentImport ! = null )
2021-09-30 22:45:09 +08:00
{
notification . CompletionText + = " Click to view." ;
notification . CompletionClickAction = ( ) = >
{
2022-06-20 17:21:37 +08:00
PresentImport ? . Invoke ( imported ) ;
2021-09-30 22:45:09 +08:00
return true ;
} ;
}
notification . State = ProgressNotificationState . Completed ;
}
return imported ;
}
2022-07-26 14:46:29 +08:00
public virtual Task < Live < TModel > ? > ImportAsUpdate ( ProgressNotification notification , ImportTask task , TModel original ) = > throw new NotImplementedException ( ) ;
2021-09-30 22:45:09 +08:00
/// <summary>
/// Import one <typeparamref name="TModel"/> from the filesystem and delete the file on success.
/// Note that this bypasses the UI flow and should only be used for special cases or testing.
/// </summary>
/// <param name="task">The <see cref="ImportTask"/> containing data about the <typeparamref name="TModel"/> to import.</param>
2022-12-12 22:56:11 +08:00
/// <param name="parameters">Parameters to further configure the import process.</param>
2021-09-30 22:45:09 +08:00
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The imported model, if successful.</returns>
2022-12-12 22:56:11 +08:00
public async Task < Live < TModel > ? > Import ( ImportTask task , ImportParameters parameters = default , CancellationToken cancellationToken = default )
2021-09-30 22:45:09 +08:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
2022-01-26 12:37:33 +08:00
Live < TModel > ? import ;
2021-09-30 22:45:09 +08:00
using ( ArchiveReader reader = task . GetReader ( ) )
2022-12-12 22:56:11 +08:00
import = await importFromArchive ( reader , parameters , cancellationToken ) . ConfigureAwait ( false ) ;
2021-09-30 22:45:09 +08:00
// We may or may not want to delete the file depending on where it is stored.
// e.g. reconstructing/repairing database with items from default storage.
// Also, not always a single file, i.e. for LegacyFilesystemReader
// TODO: Add a check to prevent files from storage to be deleted.
try
{
2023-01-23 05:19:04 +08:00
if ( import ! = null & & ShouldDeleteArchive ( task . Path ) )
task . DeleteFile ( ) ;
2021-09-30 22:45:09 +08:00
}
catch ( Exception e )
{
Logger . Error ( e , $@"Could not delete original file after import ({task})" ) ;
}
return import ;
}
/// <summary>
2022-06-20 14:25:43 +08:00
/// Create and import a model based off the provided <see cref="ArchiveReader"/>.
2021-09-30 22:45:09 +08:00
/// </summary>
2022-06-20 14:25:43 +08:00
/// <remarks>
/// This method also handled queueing the import task on a relevant import thread pool.
/// </remarks>
2021-09-30 22:45:09 +08:00
/// <param name="archive">The archive to be imported.</param>
2022-12-12 22:56:11 +08:00
/// <param name="parameters">Parameters to further configure the import process.</param>
2021-09-30 22:45:09 +08:00
/// <param name="cancellationToken">An optional cancellation token.</param>
2022-12-12 22:56:11 +08:00
private async Task < Live < TModel > ? > importFromArchive ( ArchiveReader archive , ImportParameters parameters = default , CancellationToken cancellationToken = default )
2021-09-30 22:45:09 +08:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
TModel ? model = null ;
try
{
2023-09-27 16:02:47 +08:00
model = CreateModel ( archive , parameters ) ;
2021-09-30 22:45:09 +08:00
if ( model = = null )
return null ;
}
catch ( TaskCanceledException )
{
throw ;
}
catch ( Exception e )
{
LogForModel ( model , @ $"Model creation of {archive.Name} failed." , e ) ;
return null ;
}
2022-06-20 14:36:44 +08:00
2022-12-12 22:56:11 +08:00
var scheduledImport = Task . Factory . StartNew ( ( ) = > ImportModel ( model , archive , parameters , cancellationToken ) ,
2022-06-20 14:36:44 +08:00
cancellationToken ,
TaskCreationOptions . HideScheduler ,
2022-12-12 22:56:11 +08:00
parameters . Batch ? import_scheduler_batch : import_scheduler ) ;
2022-06-20 14:36:44 +08:00
return await scheduledImport . ConfigureAwait ( false ) ;
2021-09-30 22:45:09 +08:00
}
/// <summary>
/// Silently import an item from a <typeparamref name="TModel"/>.
/// </summary>
/// <param name="item">The model to be imported.</param>
/// <param name="archive">An optional archive to use for model population.</param>
2022-12-12 22:56:11 +08:00
/// <param name="parameters">Parameters to further configure the import process.</param>
2021-09-30 22:45:09 +08:00
/// <param name="cancellationToken">An optional cancellation token.</param>
2022-12-12 22:56:11 +08:00
public virtual Live < TModel > ? ImportModel ( TModel item , ArchiveReader ? archive = null , ImportParameters parameters = default , CancellationToken cancellationToken = default ) = > Realm . Run ( realm = >
2021-09-30 22:45:09 +08:00
{
2023-10-27 20:34:30 +08:00
pauseIfNecessary ( parameters , cancellationToken ) ;
2021-09-30 22:45:09 +08:00
2022-07-07 21:38:54 +08:00
TModel ? existing ;
2021-09-30 22:45:09 +08:00
2022-12-12 22:56:11 +08:00
if ( parameters . Batch & & archive ! = null )
2022-06-14 19:34:23 +08:00
{
// this is a fast bail condition to improve large import performance.
item . Hash = computeHashFast ( archive ) ;
2021-09-30 22:45:09 +08:00
2022-06-14 19:34:23 +08:00
existing = CheckForExisting ( item , realm ) ;
2021-09-30 22:45:09 +08:00
2022-06-14 19:34:23 +08:00
if ( existing ! = null )
{
// bare minimum comparisons
//
// note that this should really be checking filesizes on disk (of existing files) for some degree of sanity.
// or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files.
if ( CanSkipImport ( existing , item ) & &
getFilenames ( existing . Files ) . SequenceEqual ( getShortenedFilenames ( archive ) . Select ( p = > p . shortened ) . OrderBy ( f = > f ) ) & &
checkAllFilesExist ( existing ) )
2021-09-30 22:45:09 +08:00
{
2022-06-14 19:34:23 +08:00
LogForModel ( item , @ $"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import." ) ;
2021-09-30 22:45:09 +08:00
2022-06-14 19:34:23 +08:00
using ( var transaction = realm . BeginWrite ( ) )
{
UndeleteForReuse ( existing ) ;
transaction . Commit ( ) ;
2021-09-30 22:45:09 +08:00
}
2022-06-14 19:34:23 +08:00
return existing . ToLive ( Realm ) ;
2021-09-30 22:45:09 +08:00
}
2022-06-14 19:34:23 +08:00
LogForModel ( item , @"Found existing (optimised) but failed pre-check." ) ;
2021-09-30 22:45:09 +08:00
}
2022-06-14 19:34:23 +08:00
}
2021-09-30 22:45:09 +08:00
2022-06-14 19:34:23 +08:00
try
{
2022-06-27 14:24:25 +08:00
// Log output here will be missing a valid hash in non-batch imports.
LogForModel ( item , $@"Beginning import from {archive?.Name ?? " unknown "}..." ) ;
2021-09-30 22:45:09 +08:00
2022-10-11 16:33:44 +08:00
List < RealmNamedFileUsage > files = new List < RealmNamedFileUsage > ( ) ;
if ( archive ! = null )
{
// Import files to the disk store.
// We intentionally delay adding to realm to avoid blocking on a write during disk operations.
foreach ( var filenames in getShortenedFilenames ( archive ) )
{
using ( Stream s = archive . GetStream ( filenames . original ) )
2022-12-12 23:56:27 +08:00
files . Add ( new RealmNamedFileUsage ( Files . Add ( s , realm , false , parameters . PreferHardLinks ) , filenames . shortened ) ) ;
2022-10-11 16:33:44 +08:00
}
}
2022-06-14 19:34:23 +08:00
using ( var transaction = realm . BeginWrite ( ) )
{
2022-10-11 16:33:44 +08:00
// Add all files to realm in one go.
// This is done ahead of the main transaction to ensure we can correctly cleanup the files, even if the import fails.
foreach ( var file in files )
{
if ( ! file . File . IsManaged )
realm . Add ( file . File , true ) ;
}
transaction . Commit ( ) ;
}
2021-09-30 22:45:09 +08:00
2022-10-11 16:33:44 +08:00
item . Files . AddRange ( files ) ;
item . Hash = ComputeHash ( item ) ;
2021-09-30 22:45:09 +08:00
2022-10-11 16:33:44 +08:00
// TODO: do we want to make the transaction this local? not 100% sure, will need further investigation.
using ( var transaction = realm . BeginWrite ( ) )
{
2022-06-14 19:34:23 +08:00
// TODO: we may want to run this outside of the transaction.
Populate ( item , archive , realm , cancellationToken ) ;
2021-09-30 22:45:09 +08:00
2022-07-07 21:38:54 +08:00
// Populate() may have adjusted file content (see SkinImporter.updateSkinIniMetadata), so regardless of whether a fast check was done earlier, let's
// check for existing items a second time.
//
// If this is ever a performance issue, the fast-check hash can be compared and trigger a skip of this second check if it still matches.
// I don't think it is a huge deal doing a second indexed check, though.
existing = CheckForExisting ( item , realm ) ;
2021-09-30 22:45:09 +08:00
2022-06-14 19:34:23 +08:00
if ( existing ! = null )
{
if ( CanReuseExisting ( existing , item ) )
2021-09-30 22:45:09 +08:00
{
2022-06-14 19:34:23 +08:00
LogForModel ( item , @ $"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import." ) ;
2021-09-30 22:45:09 +08:00
2022-06-14 19:34:23 +08:00
UndeleteForReuse ( existing ) ;
transaction . Commit ( ) ;
2021-09-30 22:45:09 +08:00
2022-06-14 19:34:23 +08:00
return existing . ToLive ( Realm ) ;
2021-09-30 22:45:09 +08:00
}
2022-06-14 19:34:23 +08:00
LogForModel ( item , @"Found existing but failed re-use check." ) ;
2021-09-30 22:45:09 +08:00
2022-06-14 19:34:23 +08:00
existing . DeletePending = true ;
2021-09-30 22:45:09 +08:00
}
2022-06-14 19:34:23 +08:00
PreImport ( item , realm ) ;
// import to store
realm . Add ( item ) ;
2021-09-30 22:45:09 +08:00
2022-12-12 22:56:11 +08:00
PostImport ( item , realm , parameters ) ;
2022-07-11 01:51:54 +08:00
2022-06-14 19:34:23 +08:00
transaction . Commit ( ) ;
2021-09-30 22:45:09 +08:00
}
2022-06-14 19:34:23 +08:00
LogForModel ( item , @"Import successfully completed!" ) ;
}
catch ( Exception e )
{
if ( ! ( e is TaskCanceledException ) )
LogForModel ( item , @"Database import or population failed and has been rolled back." , e ) ;
throw ;
}
return ( Live < TModel > ? ) item . ToLive ( Realm ) ;
} ) ;
2021-09-30 22:45:09 +08:00
2022-06-14 19:00:08 +08:00
/// <summary>
/// Any file extensions which should be included in hash creation.
/// Generally should include all file types which determine the file's uniqueness.
/// Large files should be avoided if possible.
/// </summary>
/// <remarks>
/// This is only used by the default hash implementation. If <see cref="ComputeHash"/> is overridden, it will not be used.
/// </remarks>
protected abstract string [ ] HashableFileTypes { get ; }
internal static void LogForModel ( TModel ? model , string message , Exception ? e = null )
{
string trimmedHash ;
if ( model = = null | | ! model . IsValid | | string . IsNullOrEmpty ( model . Hash ) )
trimmedHash = "?????" ;
else
trimmedHash = model . Hash . Substring ( 0 , 5 ) ;
string prefix = $"[{trimmedHash}]" ;
if ( e ! = null )
Logger . Error ( e , $"{prefix} {message}" , LoggingTarget . Database ) ;
else
Logger . Log ( $"{prefix} {message}" , LoggingTarget . Database ) ;
}
/// <summary>
/// Create a SHA-2 hash from the provided archive based on file content of all files matching <see cref="HashableFileTypes"/>.
/// </summary>
/// <remarks>
/// In the case of no matching files, a hash will be generated from the passed archive's <see cref="ArchiveReader.Name"/>.
/// </remarks>
2022-07-08 01:31:33 +08:00
public string ComputeHash ( TModel item )
2022-06-14 19:00:08 +08:00
{
// for now, concatenate all hashable files in the set to create a unique hash.
MemoryStream hashable = new MemoryStream ( ) ;
foreach ( RealmNamedFileUsage file in item . Files . Where ( f = > HashableFileTypes . Any ( ext = > f . Filename . EndsWith ( ext , StringComparison . OrdinalIgnoreCase ) ) ) . OrderBy ( f = > f . Filename ) )
{
using ( Stream s = Files . Store . GetStream ( file . File . GetStoragePath ( ) ) )
s . CopyTo ( hashable ) ;
}
if ( hashable . Length > 0 )
return hashable . ComputeSHA2Hash ( ) ;
return item . Hash ;
}
2021-09-30 22:45:09 +08:00
private string computeHashFast ( ArchiveReader reader )
{
MemoryStream hashable = new MemoryStream ( ) ;
2021-10-27 12:04:41 +08:00
foreach ( string? file in reader . Filenames . Where ( f = > HashableFileTypes . Any ( ext = > f . EndsWith ( ext , StringComparison . OrdinalIgnoreCase ) ) ) . OrderBy ( f = > f ) )
2021-09-30 22:45:09 +08:00
{
using ( Stream s = reader . GetStream ( file ) )
s . CopyTo ( hashable ) ;
}
if ( hashable . Length > 0 )
return hashable . ComputeSHA2Hash ( ) ;
return reader . Name . ComputeSHA2Hash ( ) ;
}
/// <summary>
/// Create all required <see cref="File"/>s for the provided archive, adding them to the global file store.
/// </summary>
private List < RealmNamedFileUsage > createFileInfos ( ArchiveReader reader , RealmFileStore files , Realm realm )
{
var fileInfos = new List < RealmNamedFileUsage > ( ) ;
return fileInfos ;
}
private IEnumerable < ( string original , string shortened ) > getShortenedFilenames ( ArchiveReader reader )
{
string prefix = reader . Filenames . GetCommonPrefix ( ) ;
if ( ! ( prefix . EndsWith ( '/' ) | | prefix . EndsWith ( '\\' ) ) )
prefix = string . Empty ;
foreach ( string file in reader . Filenames )
yield return ( file , file . Substring ( prefix . Length ) . ToStandardisedPath ( ) ) ;
}
/// <summary>
/// Create a barebones model from the provided archive.
/// Actual expensive population should be done in <see cref="Populate"/>; this should just prepare for duplicate checking.
/// </summary>
/// <param name="archive">The archive to create the model for.</param>
2023-09-27 16:02:47 +08:00
/// <param name="parameters">Parameters to further configure the import process.</param>
2021-09-30 22:45:09 +08:00
/// <returns>A model populated with minimal information. Returning a null will abort importing silently.</returns>
2023-09-27 16:02:47 +08:00
protected abstract TModel ? CreateModel ( ArchiveReader archive , ImportParameters parameters ) ;
2021-09-30 22:45:09 +08:00
/// <summary>
/// Populate the provided model completely from the given archive.
/// After this method, the model should be in a state ready to commit to a store.
/// </summary>
/// <param name="model">The model to populate.</param>
/// <param name="archive">The archive to use as a reference for population. May be null.</param>
/// <param name="realm">The current realm context.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
2022-01-13 15:27:07 +08:00
protected abstract void Populate ( TModel model , ArchiveReader ? archive , Realm realm , CancellationToken cancellationToken = default ) ;
2021-09-30 22:45:09 +08:00
/// <summary>
/// Perform any final actions before the import to database executes.
/// </summary>
/// <param name="model">The model prepared for import.</param>
/// <param name="realm">The current realm context.</param>
protected virtual void PreImport ( TModel model , Realm realm )
{
}
2022-06-20 17:38:37 +08:00
/// <summary>
2022-07-11 01:51:54 +08:00
/// Perform any final actions before the import has been committed to the database.
2022-06-20 17:38:37 +08:00
/// </summary>
/// <param name="model">The model prepared for import.</param>
/// <param name="realm">The current realm context.</param>
2022-12-12 22:56:11 +08:00
/// <param name="parameters">Parameters to further configure the import process.</param>
protected virtual void PostImport ( TModel model , Realm realm , ImportParameters parameters )
2022-06-20 17:38:37 +08:00
{
}
2021-09-30 22:45:09 +08:00
/// <summary>
/// Check whether an existing model already exists for a new import item.
/// </summary>
/// <param name="model">The new model proposed for import.</param>
/// <param name="realm">The current realm context.</param>
/// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
protected TModel ? CheckForExisting ( TModel model , Realm realm ) = > string . IsNullOrEmpty ( model . Hash ) ? null : realm . All < TModel > ( ) . FirstOrDefault ( b = > b . Hash = = model . Hash ) ;
/// <summary>
/// Whether import can be skipped after finding an existing import early in the process.
/// Only valid when <see cref="ComputeHash"/> is not overridden.
/// </summary>
/// <param name="existing">The existing model.</param>
/// <param name="import">The newly imported model.</param>
/// <returns>Whether to skip this import completely.</returns>
protected virtual bool CanSkipImport ( TModel existing , TModel import ) = > true ;
/// <summary>
/// After an existing <typeparamref name="TModel"/> is found during an import process, the default behaviour is to use/restore the existing
/// item and skip the import. This method allows changing that behaviour.
/// </summary>
/// <param name="existing">The existing model.</param>
/// <param name="import">The newly imported model.</param>
/// <returns>Whether the existing model should be restored and used. Returning false will delete the existing and force a re-import.</returns>
protected virtual bool CanReuseExisting ( TModel existing , TModel import ) = >
// for the best or worst, we copy and import files of a new import before checking whether
// it is a duplicate. so to check if anything has changed, we can just compare all File IDs.
getIDs ( existing . Files ) . SequenceEqual ( getIDs ( import . Files ) ) & &
2022-06-13 17:57:29 +08:00
getFilenames ( existing . Files ) . SequenceEqual ( getFilenames ( import . Files ) ) ;
2022-01-25 12:42:41 +08:00
private bool checkAllFilesExist ( TModel model ) = >
model . Files . All ( f = > Files . Storage . Exists ( f . File . GetStoragePath ( ) ) ) ;
2021-09-30 22:45:09 +08:00
2022-03-22 13:10:21 +08:00
/// <summary>
/// Called when an existing model is in a soft deleted state but being recovered.
/// </summary>
/// <param name="existing">The existing model.</param>
protected virtual void UndeleteForReuse ( TModel existing )
{
2022-04-13 13:33:28 +08:00
if ( ! existing . DeletePending )
return ;
LogForModel ( existing , $@"Existing {HumanisedModelName}'s deletion flag has been removed to allow for reuse." ) ;
2022-03-22 13:10:21 +08:00
existing . DeletePending = false ;
}
2021-09-30 22:45:09 +08:00
/// <summary>
/// Whether this specified path should be removed after successful import.
/// </summary>
/// <param name="path">The path for consideration. May be a file or a directory.</param>
/// <returns>Whether to perform deletion.</returns>
protected virtual bool ShouldDeleteArchive ( string path ) = > false ;
2023-10-27 20:34:30 +08:00
private void pauseIfNecessary ( ImportParameters importParameters , CancellationToken cancellationToken )
2023-01-09 17:54:11 +08:00
{
2023-10-27 20:34:30 +08:00
if ( ! PauseImports | | importParameters . ImportImmediately )
2023-01-09 17:54:11 +08:00
return ;
2023-01-10 00:53:41 +08:00
Logger . Log ( $@"{GetType().Name} is being paused." ) ;
2023-01-09 17:54:11 +08:00
2023-01-10 00:10:20 +08:00
while ( PauseImports )
2023-01-09 17:54:11 +08:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
Thread . Sleep ( 500 ) ;
}
2023-01-10 01:07:46 +08:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2023-01-10 00:53:41 +08:00
Logger . Log ( $@"{GetType().Name} is being resumed." ) ;
2023-01-09 17:54:11 +08:00
}
2021-09-30 22:45:09 +08:00
private IEnumerable < string > getIDs ( IEnumerable < INamedFile > files )
{
foreach ( var f in files . OrderBy ( f = > f . Filename ) )
yield return f . File . Hash ;
}
private IEnumerable < string > getFilenames ( IEnumerable < INamedFile > files )
{
foreach ( var f in files . OrderBy ( f = > f . Filename ) )
yield return f . Filename ;
}
2022-06-20 20:39:47 +08:00
public virtual string HumanisedModelName = > $"{typeof(TModel).Name.Replace(@" Info ", " ").ToLowerInvariant()}" ;
2021-09-30 22:45:09 +08:00
}
}