diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index db988a544d..278acce3e5 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -225,10 +225,10 @@ namespace osu.Game.Tests.Online this.testBeatmapManager = testBeatmapManager; } - public override Live Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public override Live Import(BeatmapSetInfo item, ArchiveReader archive = null, CancellationToken cancellationToken = default) { testBeatmapManager.AllowImport.Task.WaitSafely(); - return (testBeatmapManager.CurrentImport = base.Import(item, archive, lowPriority, cancellationToken)); + return (testBeatmapManager.CurrentImport = base.Import(item, archive, cancellationToken)); } } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index dba457c81c..51f4d23bb4 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -347,35 +347,17 @@ namespace osu.Game.Beatmaps #region Implementation of ICanAcceptFiles - public Task Import(params string[] paths) - { - return beatmapModelManager.Import(paths); - } + public Task Import(params string[] paths) => beatmapModelManager.Import(paths); - public Task Import(params ImportTask[] tasks) - { - return beatmapModelManager.Import(tasks); - } + public Task Import(params ImportTask[] tasks) => beatmapModelManager.Import(tasks); - public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) - { - return beatmapModelManager.Import(notification, tasks); - } + public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) => beatmapModelManager.Import(notification, tasks); - public Task?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) - { - return beatmapModelManager.Import(task, lowPriority, cancellationToken); - } + public Task?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) => beatmapModelManager.Import(task, lowPriority, cancellationToken); - public Task?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) - { - return beatmapModelManager.Import(archive, lowPriority, cancellationToken); - } + public Task?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) => beatmapModelManager.Import(archive, lowPriority, cancellationToken); - public Live? Import(BeatmapSetInfo item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) - { - return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken); - } + public Live? Import(BeatmapSetInfo item, ArchiveReader? archive = null, CancellationToken cancellationToken = default) => beatmapModelManager.Import(item, archive, cancellationToken); public IEnumerable HandledExtensions => beatmapModelManager.HandledExtensions; diff --git a/osu.Game/Database/ICanAcceptFiles.cs b/osu.Game/Database/ICanAcceptFiles.cs index 74fd6fcc36..3cec3b8a8e 100644 --- a/osu.Game/Database/ICanAcceptFiles.cs +++ b/osu.Game/Database/ICanAcceptFiles.cs @@ -12,14 +12,22 @@ namespace osu.Game.Database public interface ICanAcceptFiles { /// - /// Import the specified paths. + /// Import one or more items from filesystem . /// + /// + /// This will be treated as a low priority batch import if more than one path is specified. + /// This will post notifications tracking progress. + /// /// The files which should be imported. Task Import(params string[] paths); /// /// Import the specified files from the given import tasks. /// + /// + /// This will be treated as a low priority batch import if more than one path is specified. + /// This will post notifications tracking progress. + /// /// The import tasks from which the files should be imported. Task Import(params ImportTask[] tasks); diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index 90df13477e..b59636e88d 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -1,14 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using osu.Game.IO.Archives; -using osu.Game.Overlays.Notifications; - #nullable enable +using System.Collections.Generic; +using System.Threading.Tasks; +using osu.Game.Overlays.Notifications; + namespace osu.Game.Database { /// @@ -18,35 +16,14 @@ namespace osu.Game.Database public interface IModelImporter : IPostNotifications, IPostImports, ICanAcceptFiles where TModel : class, IHasGuidPrimaryKey { + /// + /// Process multiple import tasks, updating a tracking notification with progress. + /// + /// The notification to update. + /// The import tasks. + /// The imported models. Task>> Import(ProgressNotification notification, params ImportTask[] tasks); - /// - /// Import one 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. - /// - /// The containing data about the to import. - /// Whether this is a low priority import. - /// An optional cancellation token. - /// The imported model, if successful. - Task?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default); - - /// - /// Silently import an item from an . - /// - /// The archive to be imported. - /// Whether this is a low priority import. - /// An optional cancellation token. - Task?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default); - - /// - /// Silently import an item from a . - /// - /// The model to be imported. - /// An optional archive to use for model population. - /// Whether this is a low priority import. - /// An optional cancellation token. - Live? Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); - /// /// A user displayable name for the model type associated with this manager. /// diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index fbec6ea1fb..d42bf1b454 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -266,57 +266,23 @@ namespace osu.Game.Scoring }); } - public void Delete(List items, bool silent = false) - { - scoreModelManager.Delete(items, silent); - } + public void Delete(List items, bool silent = false) => scoreModelManager.Delete(items, silent); - public void Undelete(List items, bool silent = false) - { - scoreModelManager.Undelete(items, silent); - } + public void Undelete(List items, bool silent = false) => scoreModelManager.Undelete(items, silent); - public void Undelete(ScoreInfo item) - { - scoreModelManager.Undelete(item); - } + public void Undelete(ScoreInfo item) => scoreModelManager.Undelete(item); - public Task Import(params string[] paths) - { - return scoreModelManager.Import(paths); - } + public Task Import(params string[] paths) => scoreModelManager.Import(paths); - public Task Import(params ImportTask[] tasks) - { - return scoreModelManager.Import(tasks); - } + public Task Import(params ImportTask[] tasks) => scoreModelManager.Import(tasks); public IEnumerable HandledExtensions => scoreModelManager.HandledExtensions; - public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) - { - return scoreModelManager.Import(notification, tasks); - } + public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) => scoreModelManager.Import(notification, tasks); - public Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) - { - return scoreModelManager.Import(task, lowPriority, cancellationToken); - } + public Live Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) => scoreModelManager.Import(item, archive, cancellationToken); - public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) - { - return scoreModelManager.Import(archive, lowPriority, cancellationToken); - } - - public Live Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) - { - return scoreModelManager.Import(item, archive, lowPriority, cancellationToken); - } - - public bool IsAvailableLocally(ScoreInfo model) - { - return scoreModelManager.IsAvailableLocally(model); - } + public bool IsAvailableLocally(ScoreInfo model) => scoreModelManager.IsAvailableLocally(model); #endregion diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 5e1902f520..7ec922987b 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -268,37 +268,21 @@ namespace osu.Game.Skinning set => skinModelManager.PostImport = value; } - public Task Import(params string[] paths) - { - return skinModelManager.Import(paths); - } + public Task Import(params string[] paths) => skinModelManager.Import(paths); - public Task Import(params ImportTask[] tasks) - { - return skinModelManager.Import(tasks); - } + public Task Import(params ImportTask[] tasks) => skinModelManager.Import(tasks); public IEnumerable HandledExtensions => skinModelManager.HandledExtensions; - public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) - { - return skinModelManager.Import(notification, tasks); - } + public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) => skinModelManager.Import(notification, tasks); - public Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) - { - return skinModelManager.Import(task, lowPriority, cancellationToken); - } + public Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) => skinModelManager.Import(task, lowPriority, cancellationToken); - public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) - { - return skinModelManager.Import(archive, lowPriority, cancellationToken); - } + public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) => + skinModelManager.Import(archive, lowPriority, cancellationToken); - public Live Import(SkinInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) - { - return skinModelManager.Import(item, archive, lowPriority, cancellationToken); - } + public Live Import(SkinInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) => + skinModelManager.Import(item, archive, cancellationToken); #endregion @@ -323,46 +307,22 @@ namespace osu.Game.Skinning }); } + public bool Delete(SkinInfo item) => skinModelManager.Delete(item); + + public void Delete(List items, bool silent = false) => skinModelManager.Delete(items, silent); + + public void Undelete(List items, bool silent = false) => skinModelManager.Undelete(items, silent); + + public void Undelete(SkinInfo item) => skinModelManager.Undelete(item); + + public bool IsAvailableLocally(SkinInfo model) => skinModelManager.IsAvailableLocally(model); + + public void ReplaceFile(SkinInfo model, RealmNamedFileUsage file, Stream contents) => skinModelManager.ReplaceFile(model, file, contents); + + public void DeleteFile(SkinInfo model, RealmNamedFileUsage file) => skinModelManager.DeleteFile(model, file); + + public void AddFile(SkinInfo model, Stream contents, string filename) => skinModelManager.AddFile(model, contents, filename); + #endregion - - public bool Delete(SkinInfo item) - { - return skinModelManager.Delete(item); - } - - public void Delete(List items, bool silent = false) - { - skinModelManager.Delete(items, silent); - } - - public void Undelete(List items, bool silent = false) - { - skinModelManager.Undelete(items, silent); - } - - public void Undelete(SkinInfo item) - { - skinModelManager.Undelete(item); - } - - public bool IsAvailableLocally(SkinInfo model) - { - return skinModelManager.IsAvailableLocally(model); - } - - public void ReplaceFile(SkinInfo model, RealmNamedFileUsage file, Stream contents) - { - skinModelManager.ReplaceFile(model, file, contents); - } - - public void DeleteFile(SkinInfo model, RealmNamedFileUsage file) - { - skinModelManager.DeleteFile(model, file); - } - - public void AddFile(SkinInfo model, Stream contents, string filename) - { - skinModelManager.AddFile(model, contents, filename); - } } } diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index f04a0210ef..3d89dfeca9 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -39,9 +39,6 @@ namespace osu.Game.Stores protected override string[] HashableFileTypes => new[] { ".osu" }; - // protected override bool CheckLocalAvailability(RealmBeatmapSet model, System.Linq.IQueryable items) - // => base.CheckLocalAvailability(model, items) || (model.OnlineID > -1)); - private readonly BeatmapOnlineLookupQueue? onlineLookupQueue; protected BeatmapImporter(RealmAccess realm, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null) diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index e7477cfffc..82b77bfb05 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -78,22 +78,7 @@ namespace osu.Game.Stores Files = new RealmFileStore(realm, storage); } - /// - /// Import one or more items from filesystem . - /// - /// - /// This will be treated as a low priority import if more than one path is specified; use to always import at standard priority. - /// This will post notifications tracking progress. - /// - /// One or more archive locations on disk. - public Task Import(params string[] paths) - { - var notification = new ProgressNotification { State = ProgressNotificationState.Active }; - - PostNotification?.Invoke(notification); - - return Import(notification, paths.Select(p => new ImportTask(p)).ToArray()); - } + public Task Import(params string[] paths) => Import(paths.Select(p => new ImportTask(p)).ToArray()); public Task Import(params ImportTask[] tasks) { @@ -250,7 +235,7 @@ namespace osu.Game.Stores return null; } - var scheduledImport = Task.Factory.StartNew(() => Import(model, archive, lowPriority, cancellationToken), + var scheduledImport = Task.Factory.StartNew(() => Import(model, archive, cancellationToken), cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler); @@ -258,69 +243,13 @@ namespace osu.Game.Stores return await scheduledImport.ConfigureAwait(false); } - /// - /// 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. - /// - /// - /// This is only used by the default hash implementation. If is overridden, it will not be used. - /// - 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); - } - - /// - /// Whether the implementation overrides with a custom implementation. - /// Custom hash implementations must bypass the early exit in the import flow (see usage). - /// - protected virtual bool HasCustomHashFunction => false; - - /// - /// Create a SHA-2 hash from the provided archive based on file content of all files matching . - /// - /// - /// In the case of no matching files, a hash will be generated from the passed archive's . - /// - protected virtual string ComputeHash(TModel item) - { - // 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; - } - /// /// Silently import an item from a . /// /// The model to be imported. /// An optional archive to use for model population. - /// Whether this is a low priority import. /// An optional cancellation token. - public virtual Live? Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public virtual Live? Import(TModel item, ArchiveReader? archive = null, CancellationToken cancellationToken = default) { return Realm.Run(realm => { @@ -420,6 +349,61 @@ namespace osu.Game.Stores }); } + /// + /// 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. + /// + /// + /// This is only used by the default hash implementation. If is overridden, it will not be used. + /// + 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); + } + + /// + /// Whether the implementation overrides with a custom implementation. + /// Custom hash implementations must bypass the early exit in the import flow (see usage). + /// + protected virtual bool HasCustomHashFunction => false; + + /// + /// Create a SHA-2 hash from the provided archive based on file content of all files matching . + /// + /// + /// In the case of no matching files, a hash will be generated from the passed archive's . + /// + protected virtual string ComputeHash(TModel item) + { + // 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; + } + private string computeHashFast(ArchiveReader reader) { MemoryStream hashable = new MemoryStream();