mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 21:02:55 +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.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Models;
|
||||
|
||||
#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
|
||||
{
|
||||
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