mirror of
https://github.com/ppy/osu.git
synced 2025-01-22 07:47:29 +08:00
Merge pull request #15041 from peppy/realm-file-store
Add realm `FileStore`
This commit is contained in:
commit
df5a76ad9d
114
osu.Game.Tests/Database/FileStoreTests.cs
Normal file
114
osu.Game.Tests/Database/FileStoreTests.cs
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using osu.Game.Stores;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Database
|
||||||
|
{
|
||||||
|
public class FileStoreTests : RealmTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestImportFile()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
var realm = realmFactory.Context;
|
||||||
|
var files = new RealmFileStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
|
||||||
|
|
||||||
|
realm.Write(() => files.Add(testData, realm));
|
||||||
|
|
||||||
|
Assert.True(files.Storage.Exists("0/05/054edec1d0211f624fed0cbca9d4f9400b0e491c43742af2c5b0abebf0c990d8"));
|
||||||
|
Assert.True(files.Storage.Exists(realm.All<RealmFile>().First().StoragePath));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportSameFileTwice()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
var realm = realmFactory.Context;
|
||||||
|
var files = new RealmFileStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
|
||||||
|
|
||||||
|
realm.Write(() => files.Add(testData, realm));
|
||||||
|
realm.Write(() => files.Add(testData, realm));
|
||||||
|
|
||||||
|
Assert.AreEqual(1, realm.All<RealmFile>().Count());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDontPurgeReferenced()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
var realm = realmFactory.Context;
|
||||||
|
var files = new RealmFileStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
|
||||||
|
|
||||||
|
var timer = new Stopwatch();
|
||||||
|
timer.Start();
|
||||||
|
|
||||||
|
realm.Write(() =>
|
||||||
|
{
|
||||||
|
// attach the file to an arbitrary beatmap
|
||||||
|
var beatmapSet = CreateBeatmapSet(CreateRuleset());
|
||||||
|
|
||||||
|
beatmapSet.Files.Add(new RealmNamedFileUsage(file, "arbitrary.resource"));
|
||||||
|
|
||||||
|
realm.Add(beatmapSet);
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.Log($"Import complete at {timer.ElapsedMilliseconds}");
|
||||||
|
|
||||||
|
string path = file.StoragePath;
|
||||||
|
|
||||||
|
Assert.True(realm.All<RealmFile>().Any());
|
||||||
|
Assert.True(files.Storage.Exists(path));
|
||||||
|
|
||||||
|
files.Cleanup();
|
||||||
|
Logger.Log($"Cleanup complete at {timer.ElapsedMilliseconds}");
|
||||||
|
|
||||||
|
Assert.True(realm.All<RealmFile>().Any());
|
||||||
|
Assert.True(file.IsValid);
|
||||||
|
Assert.True(files.Storage.Exists(path));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPurgeUnreferenced()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
var realm = realmFactory.Context;
|
||||||
|
var files = new RealmFileStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
|
||||||
|
|
||||||
|
string path = file.StoragePath;
|
||||||
|
|
||||||
|
Assert.True(realm.All<RealmFile>().Any());
|
||||||
|
Assert.True(files.Storage.Exists(path));
|
||||||
|
|
||||||
|
files.Cleanup();
|
||||||
|
|
||||||
|
Assert.False(realm.All<RealmFile>().Any());
|
||||||
|
Assert.False(file.IsValid);
|
||||||
|
Assert.False(files.Storage.Exists(path));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,10 +5,12 @@ using System;
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Models;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
@ -70,6 +72,46 @@ namespace osu.Game.Tests.Database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static RealmBeatmapSet CreateBeatmapSet(RealmRuleset ruleset)
|
||||||
|
{
|
||||||
|
RealmFile createRealmFile() => new RealmFile { Hash = Guid.NewGuid().ToString().ComputeSHA2Hash() };
|
||||||
|
|
||||||
|
var metadata = new RealmBeatmapMetadata
|
||||||
|
{
|
||||||
|
Title = "My Love",
|
||||||
|
Artist = "Kuba Oms"
|
||||||
|
};
|
||||||
|
|
||||||
|
var beatmapSet = new RealmBeatmapSet
|
||||||
|
{
|
||||||
|
Beatmaps =
|
||||||
|
{
|
||||||
|
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Easy", },
|
||||||
|
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Normal", },
|
||||||
|
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Hard", },
|
||||||
|
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Insane", }
|
||||||
|
},
|
||||||
|
Files =
|
||||||
|
{
|
||||||
|
new RealmNamedFileUsage(createRealmFile(), "test [easy].osu"),
|
||||||
|
new RealmNamedFileUsage(createRealmFile(), "test [normal].osu"),
|
||||||
|
new RealmNamedFileUsage(createRealmFile(), "test [hard].osu"),
|
||||||
|
new RealmNamedFileUsage(createRealmFile(), "test [insane].osu"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
beatmapSet.Files.Add(new RealmNamedFileUsage(createRealmFile(), $"hitsound{i}.mp3"));
|
||||||
|
|
||||||
|
foreach (var b in beatmapSet.Beatmaps)
|
||||||
|
b.BeatmapSet = beatmapSet;
|
||||||
|
|
||||||
|
return beatmapSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static RealmRuleset CreateRuleset() =>
|
||||||
|
new RealmRuleset(0, "osu!", "osu", true);
|
||||||
|
|
||||||
private class RealmTestGame : Framework.Game
|
private class RealmTestGame : Framework.Game
|
||||||
{
|
{
|
||||||
public RealmTestGame(Func<Task> work)
|
public RealmTestGame(Func<Task> work)
|
||||||
|
20
osu.Game/Database/IHasRealmFiles.cs
Normal file
20
osu.Game/Database/IHasRealmFiles.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using osu.Game.Models;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A model that contains a list of files it is responsible for.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHasRealmFiles
|
||||||
|
{
|
||||||
|
IList<RealmNamedFileUsage> Files { get; }
|
||||||
|
|
||||||
|
string Hash { get; set; }
|
||||||
|
}
|
||||||
|
}
|
19
osu.Game/Database/INamedFile.cs
Normal file
19
osu.Game/Database/INamedFile.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// 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 osu.Game.Models;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a join model which gives a filename and scope to a <see cref="File"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface INamedFile
|
||||||
|
{
|
||||||
|
string Filename { get; set; }
|
||||||
|
|
||||||
|
RealmFile File { get; set; }
|
||||||
|
}
|
||||||
|
}
|
117
osu.Game/Models/RealmBeatmap.cs
Normal file
117
osu.Game/Models/RealmBeatmap.cs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
// 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 JetBrains.Annotations;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A single beatmap difficulty.
|
||||||
|
/// </summary>
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
[Serializable]
|
||||||
|
[MapTo("Beatmap")]
|
||||||
|
public class RealmBeatmap : RealmObject, IHasGuidPrimaryKey, IBeatmapInfo
|
||||||
|
{
|
||||||
|
[PrimaryKey]
|
||||||
|
public Guid ID { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public string DifficultyName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public RealmRuleset Ruleset { get; set; } = null!;
|
||||||
|
|
||||||
|
public RealmBeatmapDifficulty Difficulty { get; set; } = null!;
|
||||||
|
|
||||||
|
public RealmBeatmapMetadata Metadata { get; set; } = null!;
|
||||||
|
|
||||||
|
public RealmBeatmapSet? BeatmapSet { get; set; }
|
||||||
|
|
||||||
|
public BeatmapSetOnlineStatus Status
|
||||||
|
{
|
||||||
|
get => (BeatmapSetOnlineStatus)StatusInt;
|
||||||
|
set => StatusInt = (int)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MapTo(nameof(Status))]
|
||||||
|
public int StatusInt { get; set; }
|
||||||
|
|
||||||
|
public int? OnlineID { get; set; }
|
||||||
|
|
||||||
|
public double Length { get; set; }
|
||||||
|
|
||||||
|
public double BPM { get; set; }
|
||||||
|
|
||||||
|
public string Hash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public double StarRating { get; set; }
|
||||||
|
|
||||||
|
public string MD5Hash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool Hidden { get; set; }
|
||||||
|
|
||||||
|
public RealmBeatmap(RealmRuleset ruleset, RealmBeatmapDifficulty difficulty, RealmBeatmapMetadata metadata)
|
||||||
|
{
|
||||||
|
Ruleset = ruleset;
|
||||||
|
Difficulty = difficulty;
|
||||||
|
Metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
private RealmBeatmap()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Properties we may not want persisted (but also maybe no harm?)
|
||||||
|
|
||||||
|
public double AudioLeadIn { get; set; }
|
||||||
|
|
||||||
|
public float StackLeniency { get; set; } = 0.7f;
|
||||||
|
|
||||||
|
public bool SpecialStyle { get; set; }
|
||||||
|
|
||||||
|
public bool LetterboxInBreaks { get; set; }
|
||||||
|
|
||||||
|
public bool WidescreenStoryboard { get; set; }
|
||||||
|
|
||||||
|
public bool EpilepsyWarning { get; set; }
|
||||||
|
|
||||||
|
public bool SamplesMatchPlaybackRate { get; set; }
|
||||||
|
|
||||||
|
public double DistanceSpacing { get; set; }
|
||||||
|
|
||||||
|
public int BeatDivisor { get; set; }
|
||||||
|
|
||||||
|
public int GridSize { get; set; }
|
||||||
|
|
||||||
|
public double TimelineZoom { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public bool AudioEquals(RealmBeatmap? other) => other != null
|
||||||
|
&& BeatmapSet != null
|
||||||
|
&& other.BeatmapSet != null
|
||||||
|
&& BeatmapSet.Hash == other.BeatmapSet.Hash
|
||||||
|
&& Metadata.AudioFile == other.Metadata.AudioFile;
|
||||||
|
|
||||||
|
public bool BackgroundEquals(RealmBeatmap? other) => other != null
|
||||||
|
&& BeatmapSet != null
|
||||||
|
&& other.BeatmapSet != null
|
||||||
|
&& BeatmapSet.Hash == other.BeatmapSet.Hash
|
||||||
|
&& Metadata.BackgroundFile == other.Metadata.BackgroundFile;
|
||||||
|
|
||||||
|
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
|
||||||
|
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
|
||||||
|
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
|
||||||
|
IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => Difficulty;
|
||||||
|
}
|
||||||
|
}
|
45
osu.Game/Models/RealmBeatmapDifficulty.cs
Normal file
45
osu.Game/Models/RealmBeatmapDifficulty.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// 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 osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Models
|
||||||
|
{
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
[MapTo("BeatmapDifficulty")]
|
||||||
|
public class RealmBeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo
|
||||||
|
{
|
||||||
|
public float DrainRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
|
||||||
|
public float CircleSize { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
|
||||||
|
public float OverallDifficulty { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
|
||||||
|
public float ApproachRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
|
||||||
|
|
||||||
|
public double SliderMultiplier { get; set; } = 1;
|
||||||
|
public double SliderTickRate { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a shallow-clone of this <see cref="RealmBeatmapDifficulty"/>.
|
||||||
|
/// </summary>
|
||||||
|
public RealmBeatmapDifficulty Clone()
|
||||||
|
{
|
||||||
|
var diff = new RealmBeatmapDifficulty();
|
||||||
|
CopyTo(diff);
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyTo(RealmBeatmapDifficulty difficulty)
|
||||||
|
{
|
||||||
|
difficulty.ApproachRate = ApproachRate;
|
||||||
|
difficulty.DrainRate = DrainRate;
|
||||||
|
difficulty.CircleSize = CircleSize;
|
||||||
|
difficulty.OverallDifficulty = OverallDifficulty;
|
||||||
|
|
||||||
|
difficulty.SliderMultiplier = SliderMultiplier;
|
||||||
|
difficulty.SliderTickRate = SliderTickRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
osu.Game/Models/RealmBeatmapMetadata.cs
Normal file
45
osu.Game/Models/RealmBeatmapMetadata.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// 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 Newtonsoft.Json;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Models
|
||||||
|
{
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
[Serializable]
|
||||||
|
[MapTo("BeatmapMetadata")]
|
||||||
|
public class RealmBeatmapMetadata : RealmObject, IBeatmapMetadataInfo
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("title_unicode")]
|
||||||
|
public string TitleUnicode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Artist { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("artist_unicode")]
|
||||||
|
public string ArtistUnicode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Author { get; set; } = string.Empty; // eventually should be linked to a persisted User.
|
||||||
|
|
||||||
|
public string Source { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty(@"tags")]
|
||||||
|
public string Tags { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time in milliseconds to begin playing the track for preview purposes.
|
||||||
|
/// If -1, the track should begin playing at 40% of its length.
|
||||||
|
/// </summary>
|
||||||
|
public int PreviewTime { get; set; }
|
||||||
|
|
||||||
|
public string AudioFile { get; set; } = string.Empty;
|
||||||
|
public string BackgroundFile { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
78
osu.Game/Models/RealmBeatmapSet.cs
Normal file
78
osu.Game/Models/RealmBeatmapSet.cs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// 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.Linq;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Models
|
||||||
|
{
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
[MapTo("BeatmapSet")]
|
||||||
|
public class RealmBeatmapSet : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable<RealmBeatmapSet>, IBeatmapSetInfo
|
||||||
|
{
|
||||||
|
[PrimaryKey]
|
||||||
|
public Guid ID { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public int? OnlineID { get; set; }
|
||||||
|
|
||||||
|
public DateTimeOffset DateAdded { get; set; }
|
||||||
|
|
||||||
|
public IBeatmapMetadataInfo? Metadata => Beatmaps.FirstOrDefault()?.Metadata;
|
||||||
|
|
||||||
|
public IList<RealmBeatmap> Beatmaps { get; } = null!;
|
||||||
|
|
||||||
|
public IList<RealmNamedFileUsage> Files { get; } = null!;
|
||||||
|
|
||||||
|
public bool DeletePending { get; set; }
|
||||||
|
|
||||||
|
public string Hash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether deleting this beatmap set should be prohibited (due to it being a system requirement to be present).
|
||||||
|
/// </summary>
|
||||||
|
public bool Protected { get; set; }
|
||||||
|
|
||||||
|
public double MaxStarDifficulty => Beatmaps.Max(b => b.StarRating);
|
||||||
|
|
||||||
|
public double MaxLength => Beatmaps.Max(b => b.Length);
|
||||||
|
|
||||||
|
public double MaxBPM => Beatmaps.Max(b => b.BPM);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
|
||||||
|
/// The path returned is relative to the user file storage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The name of the file to get the storage path of.</param>
|
||||||
|
public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.StoragePath;
|
||||||
|
|
||||||
|
public override string ToString() => Metadata?.ToString() ?? base.ToString();
|
||||||
|
|
||||||
|
public bool Equals(RealmBeatmapSet? other)
|
||||||
|
{
|
||||||
|
if (other == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (IsManaged && other.IsManaged)
|
||||||
|
return ID == other.ID;
|
||||||
|
|
||||||
|
if (OnlineID.HasValue && other.OnlineID.HasValue)
|
||||||
|
return OnlineID == other.OnlineID;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash))
|
||||||
|
return Hash == other.Hash;
|
||||||
|
|
||||||
|
return ReferenceEquals(this, other);
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerable<IBeatmapInfo> IBeatmapSetInfo.Beatmaps => Beatmaps;
|
||||||
|
|
||||||
|
IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => Files;
|
||||||
|
}
|
||||||
|
}
|
22
osu.Game/Models/RealmFile.cs
Normal file
22
osu.Game/Models/RealmFile.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// 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.IO;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.IO;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Models
|
||||||
|
{
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
[MapTo("File")]
|
||||||
|
public class RealmFile : RealmObject, IFileInfo
|
||||||
|
{
|
||||||
|
[PrimaryKey]
|
||||||
|
public string Hash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string StoragePath => Path.Combine(Hash.Remove(1), Hash.Remove(2), Hash);
|
||||||
|
}
|
||||||
|
}
|
34
osu.Game/Models/RealmNamedFileUsage.cs
Normal file
34
osu.Game/Models/RealmNamedFileUsage.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// 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 JetBrains.Annotations;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.IO;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Models
|
||||||
|
{
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
public class RealmNamedFileUsage : EmbeddedObject, INamedFile, INamedFileUsage
|
||||||
|
{
|
||||||
|
public RealmFile File { get; set; } = null!;
|
||||||
|
|
||||||
|
public string Filename { get; set; } = null!;
|
||||||
|
|
||||||
|
public RealmNamedFileUsage(RealmFile file, string filename)
|
||||||
|
{
|
||||||
|
File = file;
|
||||||
|
Filename = filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
private RealmNamedFileUsage()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
IFileInfo INamedFileUsage.File => File;
|
||||||
|
}
|
||||||
|
}
|
63
osu.Game/Models/RealmRuleset.cs
Normal file
63
osu.Game/Models/RealmRuleset.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// 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 JetBrains.Annotations;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Models
|
||||||
|
{
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
[MapTo("Ruleset")]
|
||||||
|
public class RealmRuleset : RealmObject, IEquatable<RealmRuleset>, IRulesetInfo
|
||||||
|
{
|
||||||
|
[PrimaryKey]
|
||||||
|
public string ShortName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int? OnlineID { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string InstantiationInfo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public RealmRuleset(string shortName, string name, string instantiationInfo, int? onlineID = null)
|
||||||
|
{
|
||||||
|
ShortName = shortName;
|
||||||
|
Name = name;
|
||||||
|
InstantiationInfo = instantiationInfo;
|
||||||
|
OnlineID = onlineID;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
private RealmRuleset()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmRuleset(int? onlineID, string name, string shortName, bool available)
|
||||||
|
{
|
||||||
|
OnlineID = onlineID;
|
||||||
|
Name = name;
|
||||||
|
ShortName = shortName;
|
||||||
|
Available = available;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Available { get; set; }
|
||||||
|
|
||||||
|
public bool Equals(RealmRuleset? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo;
|
||||||
|
|
||||||
|
public override string ToString() => Name;
|
||||||
|
|
||||||
|
public RealmRuleset Clone() => new RealmRuleset
|
||||||
|
{
|
||||||
|
OnlineID = OnlineID,
|
||||||
|
Name = Name,
|
||||||
|
ShortName = ShortName,
|
||||||
|
InstantiationInfo = InstantiationInfo,
|
||||||
|
Available = Available
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
116
osu.Game/Stores/RealmFileStore.cs
Normal file
116
osu.Game/Stores/RealmFileStore.cs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// 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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.IO.Stores;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Stores
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the storing of files to the file system (and database) backing.
|
||||||
|
/// </summary>
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
public class RealmFileStore
|
||||||
|
{
|
||||||
|
private readonly RealmContextFactory realmFactory;
|
||||||
|
|
||||||
|
public readonly IResourceStore<byte[]> Store;
|
||||||
|
|
||||||
|
public readonly Storage Storage;
|
||||||
|
|
||||||
|
public RealmFileStore(RealmContextFactory realmFactory, Storage storage)
|
||||||
|
{
|
||||||
|
this.realmFactory = realmFactory;
|
||||||
|
|
||||||
|
Storage = storage.GetStorageForDirectory(@"files");
|
||||||
|
Store = new StorageBackedResourceStore(Storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a new file to the game-wide database, copying it to permanent storage if not already present.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The file data stream.</param>
|
||||||
|
/// <param name="realm">The realm instance to add to. Should already be in a transaction.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public RealmFile Add(Stream data, Realm realm)
|
||||||
|
{
|
||||||
|
string hash = data.ComputeSHA2Hash();
|
||||||
|
|
||||||
|
var existing = realm.Find<RealmFile>(hash);
|
||||||
|
|
||||||
|
var file = existing ?? new RealmFile { Hash = hash };
|
||||||
|
|
||||||
|
if (!checkFileExistsAndMatchesHash(file))
|
||||||
|
copyToStore(file, data);
|
||||||
|
|
||||||
|
if (!file.IsManaged)
|
||||||
|
realm.Add(file);
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyToStore(RealmFile file, Stream data)
|
||||||
|
{
|
||||||
|
data.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
using (var output = Storage.GetStream(file.StoragePath, FileAccess.Write))
|
||||||
|
data.CopyTo(output);
|
||||||
|
|
||||||
|
data.Seek(0, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool checkFileExistsAndMatchesHash(RealmFile file)
|
||||||
|
{
|
||||||
|
string path = file.StoragePath;
|
||||||
|
|
||||||
|
// we may be re-adding a file to fix missing store entries.
|
||||||
|
if (!Storage.Exists(path))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// even if the file already exists, check the existing checksum for safety.
|
||||||
|
using (var stream = Storage.GetStream(path))
|
||||||
|
return stream.ComputeSHA2Hash() == file.Hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
var realm = realmFactory.Context;
|
||||||
|
|
||||||
|
// can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal.
|
||||||
|
using (var transaction = realm.BeginWrite())
|
||||||
|
{
|
||||||
|
// TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707)
|
||||||
|
var files = realm.All<RealmFile>().ToList();
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
if (file.BacklinksCount > 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Storage.Delete(file.StoragePath);
|
||||||
|
realm.Remove(file);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error(e, $@"Could not delete databased file {file.Hash}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user