From 9358b5d19cfcf90fbb0cddce05f3035317b915c9 Mon Sep 17 00:00:00 2001 From: chyyran Date: Tue, 27 Sep 2022 00:20:24 -0400 Subject: [PATCH 1/3] content: Add contentlibrary API --- .../Filesystem/Library/IContentLibrary.cs | 16 ++++++ .../Library/IContentLibraryStore.cs | 13 +++++ .../Filesystem/Library/ContentLibrary.cs | 23 ++++++++ .../Model/Database/ContentLibraryStore.cs | 56 +++++++++++++++++++ .../Database/Models/ContentLibraryModel.cs | 20 +++++++ .../Model/Database/Models/DatabaseContext.cs | 4 ++ .../StoreProviderComposable.cs | 5 ++ 7 files changed, 137 insertions(+) create mode 100644 src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibrary.cs create mode 100644 src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibraryStore.cs create mode 100644 src/Snowflake.Framework/Filesystem/Library/ContentLibrary.cs create mode 100644 src/Snowflake.Framework/Model/Database/ContentLibraryStore.cs create mode 100644 src/Snowflake.Framework/Model/Database/Models/ContentLibraryModel.cs diff --git a/src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibrary.cs b/src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibrary.cs new file mode 100644 index 000000000..91157a18f --- /dev/null +++ b/src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibrary.cs @@ -0,0 +1,16 @@ +using Snowflake.Model.Game; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Filesystem.Library +{ + public interface IContentLibrary + { + public Guid LibraryID { get; } + + public IDirectory OpenLibrary(IGame game); + } +} diff --git a/src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibraryStore.cs b/src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibraryStore.cs new file mode 100644 index 000000000..28aa33181 --- /dev/null +++ b/src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibraryStore.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Filesystem.Library +{ + public interface IContentLibraryStore + { + public IEnumerable GetLibraries(); + } +} diff --git a/src/Snowflake.Framework/Filesystem/Library/ContentLibrary.cs b/src/Snowflake.Framework/Filesystem/Library/ContentLibrary.cs new file mode 100644 index 000000000..f401f9088 --- /dev/null +++ b/src/Snowflake.Framework/Filesystem/Library/ContentLibrary.cs @@ -0,0 +1,23 @@ +using Snowflake.Model.Game; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Zio.FileSystems; + +namespace Snowflake.Filesystem.Library +{ + internal class ContentLibrary : IContentLibrary + { + public ContentLibrary(SubFileSystem subFs) + { + + } + + public IDirectory OpenLibrary(IGame game) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Snowflake.Framework/Model/Database/ContentLibraryStore.cs b/src/Snowflake.Framework/Model/Database/ContentLibraryStore.cs new file mode 100644 index 000000000..0fcc749fc --- /dev/null +++ b/src/Snowflake.Framework/Model/Database/ContentLibraryStore.cs @@ -0,0 +1,56 @@ +using Microsoft.EntityFrameworkCore; +using Snowflake.Filesystem.Library; +using Snowflake.Model.Database.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Zio; + +namespace Snowflake.Model.Database +{ + internal class ContentLibraryStore : IContentLibraryStore + { + private DbContextOptionsBuilder Options { get; set; } + public IFileSystem FileSystem { get; } + + public ContentLibraryStore(IFileSystem baseFileSystem, DbContextOptionsBuilder options) + { + this.Options = options; + using var context = new DatabaseContext(Options.Options); + context.Database.Migrate(); + + this.FileSystem = baseFileSystem; + } + + public IEnumerable GetLibraries() + { + using var context = new DatabaseContext(this.Options.Options); + // model path is Zio-format + return context.ContentLibraries.AsEnumerable() + .Select(model => new ContentLibrary( + this.FileSystem.GetOrCreateSubFileSystem(model.Path))); + } + + public IContentLibrary CreateLibrary(DirectoryInfo directory) + { + if (!directory.Exists) + { + directory.Create(); + } + + UPath path = this.FileSystem.ConvertPathFromInternal(directory.FullName); + using var context = new DatabaseContext(this.Options.Options); + if (context.ContentLibraries.Find(path) is ContentLibraryModel library) + { + return new ContentLibrary(this.FileSystem.GetOrCreateSubFileSystem(library.Path)); + } + + context.ContentLibraries.Add(new() { Path = (string)path }); + context.SaveChanges(); + return new ContentLibrary(this.FileSystem.GetOrCreateSubFileSystem(path)); + } + } +} diff --git a/src/Snowflake.Framework/Model/Database/Models/ContentLibraryModel.cs b/src/Snowflake.Framework/Model/Database/Models/ContentLibraryModel.cs new file mode 100644 index 000000000..ef36d0570 --- /dev/null +++ b/src/Snowflake.Framework/Model/Database/Models/ContentLibraryModel.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Model.Database.Models +{ + internal class ContentLibraryModel + { + public string Path { get; set; } + + internal static void SetupModel(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasKey(p => p.Path); + } + } +} diff --git a/src/Snowflake.Framework/Model/Database/Models/DatabaseContext.cs b/src/Snowflake.Framework/Model/Database/Models/DatabaseContext.cs index 53b3fd13a..5c61f0a10 100644 --- a/src/Snowflake.Framework/Model/Database/Models/DatabaseContext.cs +++ b/src/Snowflake.Framework/Model/Database/Models/DatabaseContext.cs @@ -24,6 +24,8 @@ internal class DatabaseContext : DbContext public DbSet PortDeviceEntries { get; set; } + public DbSet ContentLibraries { get; set; } + public DatabaseContext(DbContextOptions options) : base(options) { @@ -46,6 +48,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) GameRecordConfigurationProfileModel.SetupModel(modelBuilder); PortDeviceEntryModel.SetupModel(modelBuilder); + + ContentLibraryModel.SetupModel(modelBuilder); } } } diff --git a/src/Snowflake.Support.StoreProviders/StoreProviderComposable.cs b/src/Snowflake.Support.StoreProviders/StoreProviderComposable.cs index aad6d86af..11db4ab3d 100644 --- a/src/Snowflake.Support.StoreProviders/StoreProviderComposable.cs +++ b/src/Snowflake.Support.StoreProviders/StoreProviderComposable.cs @@ -4,6 +4,7 @@ using System.Text; using Microsoft.EntityFrameworkCore; using Snowflake.Extensibility.Configuration; +using Snowflake.Filesystem.Library; using Snowflake.Input.Controller; using Snowflake.Loader; using Snowflake.Model.Database; @@ -79,6 +80,10 @@ public void Compose(IModule module, Loader.IServiceRepository serviceContainer) var portStore = new EmulatedPortStore(optionsBuilder); serviceContainer.Get() .RegisterService(portStore); + + var contentStore = new ContentLibraryStore(physicalFs, optionsBuilder); + serviceContainer.Get() + .RegisterService(contentStore); } } } From eee10d7731af3eeb23c87b61d50f7c8d2083de34 Mon Sep 17 00:00:00 2001 From: chyyran Date: Tue, 27 Sep 2022 19:36:37 -0400 Subject: [PATCH 2/3] content: add LibraryID back to contentLibrary --- .../Filesystem/Library/ContentLibrary.cs | 6 ++++-- .../Model/Database/ContentLibraryStore.cs | 12 +++++++----- .../Model/Database/Models/ContentLibraryModel.cs | 7 ++++++- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Snowflake.Framework/Filesystem/Library/ContentLibrary.cs b/src/Snowflake.Framework/Filesystem/Library/ContentLibrary.cs index f401f9088..fea09b276 100644 --- a/src/Snowflake.Framework/Filesystem/Library/ContentLibrary.cs +++ b/src/Snowflake.Framework/Filesystem/Library/ContentLibrary.cs @@ -10,11 +10,13 @@ namespace Snowflake.Filesystem.Library { internal class ContentLibrary : IContentLibrary { - public ContentLibrary(SubFileSystem subFs) + public ContentLibrary(Guid libraryId, SubFileSystem subFs) { - + this.LibraryID = libraryId; } + public Guid LibraryID { get; } + public IDirectory OpenLibrary(IGame game) { throw new NotImplementedException(); diff --git a/src/Snowflake.Framework/Model/Database/ContentLibraryStore.cs b/src/Snowflake.Framework/Model/Database/ContentLibraryStore.cs index 0fcc749fc..6f214176f 100644 --- a/src/Snowflake.Framework/Model/Database/ContentLibraryStore.cs +++ b/src/Snowflake.Framework/Model/Database/ContentLibraryStore.cs @@ -30,7 +30,7 @@ public IEnumerable GetLibraries() using var context = new DatabaseContext(this.Options.Options); // model path is Zio-format return context.ContentLibraries.AsEnumerable() - .Select(model => new ContentLibrary( + .Select(model => new ContentLibrary(model.LibraryId, this.FileSystem.GetOrCreateSubFileSystem(model.Path))); } @@ -43,14 +43,16 @@ public IContentLibrary CreateLibrary(DirectoryInfo directory) UPath path = this.FileSystem.ConvertPathFromInternal(directory.FullName); using var context = new DatabaseContext(this.Options.Options); - if (context.ContentLibraries.Find(path) is ContentLibraryModel library) + if (context.ContentLibraries.Where(l => l.Path == path) is ContentLibraryModel library) { - return new ContentLibrary(this.FileSystem.GetOrCreateSubFileSystem(library.Path)); + return new ContentLibrary(library.LibraryId, this.FileSystem.GetOrCreateSubFileSystem(library.Path)); } - context.ContentLibraries.Add(new() { Path = (string)path }); + var libraryId = Guid.NewGuid(); + + context.ContentLibraries.Add(new() { Path = (string)path, LibraryId = libraryId }); context.SaveChanges(); - return new ContentLibrary(this.FileSystem.GetOrCreateSubFileSystem(path)); + return new ContentLibrary(libraryId, this.FileSystem.GetOrCreateSubFileSystem(path)); } } } diff --git a/src/Snowflake.Framework/Model/Database/Models/ContentLibraryModel.cs b/src/Snowflake.Framework/Model/Database/Models/ContentLibraryModel.cs index ef36d0570..13a580e34 100644 --- a/src/Snowflake.Framework/Model/Database/Models/ContentLibraryModel.cs +++ b/src/Snowflake.Framework/Model/Database/Models/ContentLibraryModel.cs @@ -11,10 +11,15 @@ internal class ContentLibraryModel { public string Path { get; set; } + public Guid LibraryId { get; set; } internal static void SetupModel(ModelBuilder modelBuilder) { modelBuilder.Entity() - .HasKey(p => p.Path); + .HasKey(p => p.LibraryId); + + modelBuilder.Entity() + .Property(p => p.Path) + .IsRequired(); } } } From 99e7ed97dba9194969ce6e420134de9ff08d2572 Mon Sep 17 00:00:00 2001 From: chyyran Date: Mon, 3 Oct 2022 00:28:02 -0400 Subject: [PATCH 3/3] api: sketch for content library API --- .../Filesystem/Library/IContentLibrary.cs | 5 +- .../Library/IContentLibraryStore.cs | 11 +- .../LibraryExtensions/IGameFileExtension.cs | 2 +- .../Model/Records/File/IFileRecord.cs | 2 +- .../Model/Records/Game/IGameRecord.cs | 1 + .../Model/Filesystem/ContentLibraryType.cs | 31 ++ .../SnowflakeGraphQLExtensions.cs | 8 +- .../Model/ContentLibraryTest.cs | 65 ++++ .../Model/GameLibraryIntegrationTests.cs | 5 +- .../Filesystem/InMemoryFileGuidProvider.cs | 26 ++ .../Filesystem/Library/ContentLibrary.cs | 15 +- .../Model/Database/ContentLibraryStore.cs | 59 ++- .../Database/Extensions/RecordExtensions.cs | 12 +- .../Model/Database/GameRecordLibrary.cs | 1 + ...221003040212_AddContentLibrary.Designer.cs | 341 ++++++++++++++++++ .../20221003040212_AddContentLibrary.cs | 61 ++++ .../DatabaseContextModelSnapshot.cs | 69 +++- .../Database/Models/ContentLibraryModel.cs | 11 +- .../Model/Database/Models/RecordModel.cs | 2 + .../LibraryExtensions/GameFileExtension.cs | 4 +- .../GameFileExtensionProvider.cs | 19 +- .../Model/Records/GameRecord.cs | 4 +- .../Snowflake - Backup.Framework.csproj | 46 --- .../Mutations/Game/CreateGameInput.cs | 7 + .../Mutations/Game/GameMutations.cs | 12 +- .../Queries/Filesystem/FilesystemQueries.cs | 6 + .../Game/LibraryExtensions/GameFileQueries.cs | 16 +- .../StoreProviderComposable.cs | 14 +- 28 files changed, 760 insertions(+), 95 deletions(-) create mode 100644 src/Snowflake.Framework.Remoting.GraphQL/Model/Filesystem/ContentLibraryType.cs create mode 100644 src/Snowflake.Framework.Tests/Model/ContentLibraryTest.cs create mode 100644 src/Snowflake.Framework/Filesystem/InMemoryFileGuidProvider.cs create mode 100644 src/Snowflake.Framework/Model/Database/Migrations/20221003040212_AddContentLibrary.Designer.cs create mode 100644 src/Snowflake.Framework/Model/Database/Migrations/20221003040212_AddContentLibrary.cs delete mode 100644 src/Snowflake.Framework/Snowflake - Backup.Framework.csproj diff --git a/src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibrary.cs b/src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibrary.cs index 91157a18f..b8a8771c4 100644 --- a/src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibrary.cs +++ b/src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibrary.cs @@ -1,6 +1,7 @@ using Snowflake.Model.Game; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -10,7 +11,7 @@ namespace Snowflake.Filesystem.Library public interface IContentLibrary { public Guid LibraryID { get; } - - public IDirectory OpenLibrary(IGame game); + public DirectoryInfo Path { get; } + public IDirectory OpenRecordLibrary(Guid recordId); } } diff --git a/src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibraryStore.cs b/src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibraryStore.cs index 28aa33181..06fdf4b89 100644 --- a/src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibraryStore.cs +++ b/src/Snowflake.Framework.Primitives/Filesystem/Library/IContentLibraryStore.cs @@ -1,5 +1,8 @@ -using System; +using Snowflake.Model.Records; +using Snowflake.Model.Records.Game; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -8,6 +11,10 @@ namespace Snowflake.Filesystem.Library { public interface IContentLibraryStore { - public IEnumerable GetLibraries(); + IContentLibrary CreateLibrary(DirectoryInfo dirInfo); + IContentLibrary? GetRecordLibrary(IRecord record); + IEnumerable GetLibraries(); + IContentLibrary? GetLibrary(Guid libraryId); + void SetRecordLibrary(IContentLibrary library, IRecord record); } } diff --git a/src/Snowflake.Framework.Primitives/Model/Game/LibraryExtensions/IGameFileExtension.cs b/src/Snowflake.Framework.Primitives/Model/Game/LibraryExtensions/IGameFileExtension.cs index 9359277a0..7efcd3bfe 100644 --- a/src/Snowflake.Framework.Primitives/Model/Game/LibraryExtensions/IGameFileExtension.cs +++ b/src/Snowflake.Framework.Primitives/Model/Game/LibraryExtensions/IGameFileExtension.cs @@ -127,7 +127,7 @@ public interface IGameFileExtension : IGameExtension } /// - /// Fluent extensions to provide access to game configuration access. + /// Fluent extensions to provide access to game filesystem access. /// public static class GameFileExtensionExtensions { diff --git a/src/Snowflake.Framework.Primitives/Model/Records/File/IFileRecord.cs b/src/Snowflake.Framework.Primitives/Model/Records/File/IFileRecord.cs index 79491f4f9..d0272b874 100644 --- a/src/Snowflake.Framework.Primitives/Model/Records/File/IFileRecord.cs +++ b/src/Snowflake.Framework.Primitives/Model/Records/File/IFileRecord.cs @@ -8,7 +8,7 @@ namespace Snowflake.Model.Records.File /// /// /// The only difference between an and an - /// is that the mimetypf of an must be known. If so, then + /// is that the mimetype of an must be known. If so, then /// metadata can be recorded for it within a , relative to /// the manifested it wraps. /// diff --git a/src/Snowflake.Framework.Primitives/Model/Records/Game/IGameRecord.cs b/src/Snowflake.Framework.Primitives/Model/Records/Game/IGameRecord.cs index ad15d081b..2c36bb2e8 100644 --- a/src/Snowflake.Framework.Primitives/Model/Records/Game/IGameRecord.cs +++ b/src/Snowflake.Framework.Primitives/Model/Records/Game/IGameRecord.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Snowflake.Filesystem.Library; using Snowflake.Model.Game; using Snowflake.Model.Records.File; diff --git a/src/Snowflake.Framework.Remoting.GraphQL/Model/Filesystem/ContentLibraryType.cs b/src/Snowflake.Framework.Remoting.GraphQL/Model/Filesystem/ContentLibraryType.cs new file mode 100644 index 000000000..24a4419ff --- /dev/null +++ b/src/Snowflake.Framework.Remoting.GraphQL/Model/Filesystem/ContentLibraryType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using Snowflake.Filesystem.Library; +using Snowflake.Installation.Extensibility; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Remoting.GraphQL.Model.Filesystem +{ + public sealed class ContentLibraryType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name("ContentLibrary") + .Description("Describes a content library."); + + descriptor.Field(p => p.LibraryID) + .Name("libraryId") + .Description("The unique ID of the content library.") + .Type>(); + + descriptor.Field(p => p.Path) + .Name("path") + .Description("The actual path of the library on disk.") + .Type>(); + } + } +} diff --git a/src/Snowflake.Framework.Remoting.GraphQL/SnowflakeGraphQLExtensions.cs b/src/Snowflake.Framework.Remoting.GraphQL/SnowflakeGraphQLExtensions.cs index c46d39e3b..e8e1fbcfc 100644 --- a/src/Snowflake.Framework.Remoting.GraphQL/SnowflakeGraphQLExtensions.cs +++ b/src/Snowflake.Framework.Remoting.GraphQL/SnowflakeGraphQLExtensions.cs @@ -47,10 +47,10 @@ public static class SnowflakeGraphQLExtensions public static T SnowflakeService(this IResolverContext context) where T : class { - if (!context.ContextData.TryGetValue(SnowflakeGraphQLExtensions.ServicesNamespace, out object container)) - return null; - var serviceContainer = (IServiceContainer)container; - return serviceContainer.Get(); + if (context.ContextData.TryGetValue(SnowflakeGraphQLExtensions.ServicesNamespace, out object container) && (container is IServiceContainer serviceContainer)) + return serviceContainer.Get(); + + return null; } /// diff --git a/src/Snowflake.Framework.Tests/Model/ContentLibraryTest.cs b/src/Snowflake.Framework.Tests/Model/ContentLibraryTest.cs new file mode 100644 index 000000000..775d8d221 --- /dev/null +++ b/src/Snowflake.Framework.Tests/Model/ContentLibraryTest.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore; +using Snowflake.Model.Database; +using Snowflake.Model.Database.Models; +using Snowflake.Model.Game; +using Snowflake.Tests; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Zio.FileSystems; + +namespace Snowflake.Model.Records.Tests +{ + public class ContentLibraryTest + { + [Fact] + public void CreateLibraryTest() + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlite($"Data Source={Path.GetTempFileName()}"); + + var store = new ContentLibraryStore(new PhysicalFileSystem(), optionsBuilder); + + var tempDir = TestUtilities.GetTemporaryDirectory(); +#pragma warning disable CS0618 // Type or member is obsolete + var library = store.CreateLibrary(tempDir.UnsafeGetPath()); +#pragma warning restore CS0618 // Type or member is obsolete + + var recordGuid = Guid.NewGuid(); + library.OpenRecordLibrary(recordGuid); + + Assert.True(tempDir.ContainsDirectory(recordGuid.ToString())); + Assert.Equal(library.LibraryID, store.GetLibrary(library.LibraryID).LibraryID); + } + + + [Fact] + public void CreateLibraryForGameTest() + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlite($"Data Source={Path.GetTempFileName()}"); + + var store = new ContentLibraryStore(new PhysicalFileSystem(), optionsBuilder); + + var tempDir = TestUtilities.GetTemporaryDirectory(); +#pragma warning disable CS0618 // Type or member is obsolete + var library = store.CreateLibrary(tempDir.UnsafeGetPath()); +#pragma warning restore CS0618 // Type or member is obsolete + + var glib = new GameRecordLibrary(optionsBuilder); + var gl = new GameLibrary(glib); + var game = gl.CreateGame("NINTENDO_NES"); + + store.SetRecordLibrary(library, game.Record); + Assert.Equal(library.LibraryID, store.GetRecordLibrary(game.Record).LibraryID); + + var library2 = store.CreateLibrary(TestUtilities.GetTemporaryDirectory().UnsafeGetPath()); + store.SetRecordLibrary(library2, game.Record); + Assert.Equal(library2.LibraryID, store.GetRecordLibrary(game.Record).LibraryID); + } + } +} diff --git a/src/Snowflake.Framework.Tests/Model/GameLibraryIntegrationTests.cs b/src/Snowflake.Framework.Tests/Model/GameLibraryIntegrationTests.cs index 0d4f604fe..3554af0b5 100644 --- a/src/Snowflake.Framework.Tests/Model/GameLibraryIntegrationTests.cs +++ b/src/Snowflake.Framework.Tests/Model/GameLibraryIntegrationTests.cs @@ -30,7 +30,7 @@ public void GameLibraryIntegrationCreate_Test() optionsBuilder.UseSqlite($"Data Source={Path.GetTempFileName()}"); var glib = new GameRecordLibrary(optionsBuilder); var gl = new GameLibrary(glib); - var game = gl.CreateGame("NINTENDO_NES"); + gl.CreateGame("NINTENDO_NES"); } [Fact] @@ -212,7 +212,7 @@ public async Task GameLibraryIntegrationCreateAsync_Test() optionsBuilder.UseSqlite($"Data Source={Path.GetTempFileName()}"); var glib = new GameRecordLibrary(optionsBuilder); var gl = new GameLibrary(glib); - var game = await gl.CreateGameAsync("NINTENDO_NES"); + await gl.CreateGameAsync("NINTENDO_NES"); } [Fact] @@ -324,6 +324,7 @@ public async Task GameLibraryIntegrationUpdateAsync_Test() var glib = new GameRecordLibrary(optionsBuilder); var flib = new FileRecordLibrary(optionsBuilder); + var gl = new GameLibrary(glib); gl.AddExtension(new GameFileExtensionProvider(flib, gfs)); diff --git a/src/Snowflake.Framework/Filesystem/InMemoryFileGuidProvider.cs b/src/Snowflake.Framework/Filesystem/InMemoryFileGuidProvider.cs new file mode 100644 index 000000000..0df7e471d --- /dev/null +++ b/src/Snowflake.Framework/Filesystem/InMemoryFileGuidProvider.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Filesystem +{ + public class InMemoryFileGuidProvider : IFileGuidProvider + { + public static InMemoryFileGuidProvider GuidProvider { get; } = new(); + private ConcurrentDictionary GuidStore { get; } = new ConcurrentDictionary(); + + public void SetGuid(FileInfo rawInfo, Guid guid) + { + this.GuidStore.TryAdd(rawInfo, guid); + } + + public bool TryGetGuid(FileInfo rawInfo, out Guid guid) + { + return this.GuidStore.TryGetValue(rawInfo, out guid); + } + } +} diff --git a/src/Snowflake.Framework/Filesystem/Library/ContentLibrary.cs b/src/Snowflake.Framework/Filesystem/Library/ContentLibrary.cs index fea09b276..44baf42ff 100644 --- a/src/Snowflake.Framework/Filesystem/Library/ContentLibrary.cs +++ b/src/Snowflake.Framework/Filesystem/Library/ContentLibrary.cs @@ -1,25 +1,34 @@ using Snowflake.Model.Game; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using Zio; using Zio.FileSystems; namespace Snowflake.Filesystem.Library { internal class ContentLibrary : IContentLibrary { - public ContentLibrary(Guid libraryId, SubFileSystem subFs) + public ContentLibrary(Guid libraryId, IDirectory rootDirectory) { this.LibraryID = libraryId; + this.Root = rootDirectory; } public Guid LibraryID { get; } - public IDirectory OpenLibrary(IGame game) +#pragma warning disable CS0618 // Type or member is obsolete + public DirectoryInfo Path => this.Root.UnsafeGetPath(); +#pragma warning restore CS0618 // Type or member is obsolete + + private IDirectory Root { get; } + + public IDirectory OpenRecordLibrary(Guid recordId) { - throw new NotImplementedException(); + return this.Root.OpenDirectory(recordId.ToString()).AsIndelible(); } } } diff --git a/src/Snowflake.Framework/Model/Database/ContentLibraryStore.cs b/src/Snowflake.Framework/Model/Database/ContentLibraryStore.cs index 6f214176f..8a1cffaa6 100644 --- a/src/Snowflake.Framework/Model/Database/ContentLibraryStore.cs +++ b/src/Snowflake.Framework/Model/Database/ContentLibraryStore.cs @@ -1,6 +1,12 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyModel; +using Snowflake.Filesystem; using Snowflake.Filesystem.Library; +using Snowflake.Model.Database.Exceptions; +using Snowflake.Model.Database.Extensions; using Snowflake.Model.Database.Models; +using Snowflake.Model.Records; +using Snowflake.Model.Records.Game; using System; using System.Collections.Generic; using System.IO; @@ -30,29 +36,62 @@ public IEnumerable GetLibraries() using var context = new DatabaseContext(this.Options.Options); // model path is Zio-format return context.ContentLibraries.AsEnumerable() - .Select(model => new ContentLibrary(model.LibraryId, - this.FileSystem.GetOrCreateSubFileSystem(model.Path))); + .Select(model => new ContentLibrary(model.LibraryID, + new Filesystem.Directory(this.FileSystem.GetOrCreateSubFileSystem(model.Path)))); } - public IContentLibrary CreateLibrary(DirectoryInfo directory) + public IContentLibrary CreateLibrary(DirectoryInfo dirInfo) { - if (!directory.Exists) + if (!dirInfo.Exists) { - directory.Create(); + dirInfo.Create(); } - UPath path = this.FileSystem.ConvertPathFromInternal(directory.FullName); using var context = new DatabaseContext(this.Options.Options); - if (context.ContentLibraries.Where(l => l.Path == path) is ContentLibraryModel library) + if (context.ContentLibraries.Where(l => l.Path == dirInfo.FullName) is ContentLibraryModel library) { - return new ContentLibrary(library.LibraryId, this.FileSystem.GetOrCreateSubFileSystem(library.Path)); + return new ContentLibrary(library.LibraryID, new Filesystem.Directory(this.FileSystem.GetOrCreateSubFileSystem(this.FileSystem.ConvertPathFromInternal(library.Path)))); } var libraryId = Guid.NewGuid(); - context.ContentLibraries.Add(new() { Path = (string)path, LibraryId = libraryId }); + context.ContentLibraries.Add(new() { Path = dirInfo.FullName, LibraryID = libraryId }); + context.SaveChanges(); + return new ContentLibrary(libraryId, + new Filesystem.Directory(this.FileSystem.GetOrCreateSubFileSystem(this.FileSystem.ConvertPathFromInternal(dirInfo.FullName)))); + } + + + public IContentLibrary? GetLibrary(Guid libraryId) + { + using var context = new DatabaseContext(this.Options.Options); + if (context.ContentLibraries.Find(libraryId) is ContentLibraryModel model) + { + return new ContentLibrary(model.LibraryID, new Filesystem.Directory(this.FileSystem.GetOrCreateSubFileSystem(this.FileSystem.ConvertPathFromInternal(model.Path)))); + } + return null; + } + + public IContentLibrary? GetRecordLibrary(IRecord record) + { + using var context = new DatabaseContext(this.Options.Options); + if (context.ContentLibraries.FirstOrDefault(g => g.Records.Select(g => g.RecordID).Contains(record.RecordID)) is ContentLibraryModel model) + { + return new ContentLibrary(model.LibraryID, new Filesystem.Directory(this.FileSystem.GetOrCreateSubFileSystem(this.FileSystem.ConvertPathFromInternal(model.Path)))); + } + return null; + } + + public void SetRecordLibrary(IContentLibrary library, IRecord record) + { + using var context = new DatabaseContext(this.Options.Options); + RecordModel? recordModel = context.Records.Find(record.RecordID); + if (recordModel == null) + { + throw new DependentEntityNotExistsException(record.RecordID); + } + recordModel.ContentLibrary = library.AsModel(); context.SaveChanges(); - return new ContentLibrary(libraryId, this.FileSystem.GetOrCreateSubFileSystem(path)); } } } diff --git a/src/Snowflake.Framework/Model/Database/Extensions/RecordExtensions.cs b/src/Snowflake.Framework/Model/Database/Extensions/RecordExtensions.cs index bcc10dacb..cd478132e 100644 --- a/src/Snowflake.Framework/Model/Database/Extensions/RecordExtensions.cs +++ b/src/Snowflake.Framework/Model/Database/Extensions/RecordExtensions.cs @@ -6,6 +6,7 @@ using Snowflake.Model.Records; using Snowflake.Model.Records.File; using Snowflake.Model.Records.Game; +using Snowflake.Filesystem.Library; namespace Snowflake.Model.Database.Extensions { @@ -18,7 +19,7 @@ public static GameRecordModel AsModel(this IGameRecord @this) PlatformID = @this.PlatformID, RecordID = @this.RecordID, RecordType = "game", - Metadata = @this.Metadata.AsModel() + Metadata = @this.Metadata.AsModel(), }; } @@ -43,5 +44,14 @@ public static FileRecordModel AsModel(this (IFile file, string mimetype) @this) Metadata = new MetadataCollection(@this.file.FileGuid).AsModel() }; } + + public static ContentLibraryModel AsModel(this IContentLibrary @this) + { + return new ContentLibraryModel() + { + LibraryID = @this.LibraryID, + Path = @this.Path.FullName, + }; + } } } diff --git a/src/Snowflake.Framework/Model/Database/GameRecordLibrary.cs b/src/Snowflake.Framework/Model/Database/GameRecordLibrary.cs index f95cb0033..b5635e4af 100644 --- a/src/Snowflake.Framework/Model/Database/GameRecordLibrary.cs +++ b/src/Snowflake.Framework/Model/Database/GameRecordLibrary.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Snowflake.Filesystem.Library; using Snowflake.Model.Database.Extensions; using Snowflake.Model.Database.Models; using Snowflake.Model.Game; diff --git a/src/Snowflake.Framework/Model/Database/Migrations/20221003040212_AddContentLibrary.Designer.cs b/src/Snowflake.Framework/Model/Database/Migrations/20221003040212_AddContentLibrary.Designer.cs new file mode 100644 index 000000000..5f422d38b --- /dev/null +++ b/src/Snowflake.Framework/Model/Database/Migrations/20221003040212_AddContentLibrary.Designer.cs @@ -0,0 +1,341 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Snowflake.Model.Database.Models; + +#nullable disable + +namespace Snowflake.Model.Database.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20221003040212_AddContentLibrary")] + partial class AddContentLibrary + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); + + modelBuilder.Entity("Snowflake.Model.Database.Models.ConfigurationProfileModel", b => + { + b.Property("ValueCollectionGuid") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ConfigurationSource") + .HasColumnType("TEXT"); + + b.HasKey("ValueCollectionGuid"); + + b.ToTable("ConfigurationProfiles"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.ConfigurationValueModel", b => + { + b.Property("Guid") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("OptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SectionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ValueCollectionGuid") + .HasColumnType("TEXT"); + + b.Property("ValueType") + .HasColumnType("INTEGER"); + + b.HasKey("Guid"); + + b.HasIndex("ValueCollectionGuid"); + + b.ToTable("ConfigurationValues"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.ContentLibraryModel", b => + { + b.Property("LibraryID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LibraryID"); + + b.ToTable("ContentLibraries"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.ControllerElementMappingCollectionModel", b => + { + b.Property("ProfileID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ControllerID") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DriverType") + .HasColumnType("INTEGER"); + + b.Property("ProfileName") + .HasColumnType("TEXT"); + + b.Property("VendorID") + .HasColumnType("INTEGER"); + + b.HasKey("ProfileID"); + + b.ToTable("ControllerElementMappings"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.ControllerElementMappingModel", b => + { + b.Property("ProfileID") + .HasColumnType("TEXT"); + + b.Property("LayoutElement") + .HasColumnType("TEXT"); + + b.Property("DeviceCapability") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ProfileID", "LayoutElement"); + + b.ToTable("MappedControllerElements"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.GameRecordConfigurationProfileModel", b => + { + b.Property("ProfileID") + .HasColumnType("TEXT"); + + b.Property("GameID") + .HasColumnType("TEXT"); + + b.Property("ConfigurationSource") + .HasColumnType("TEXT"); + + b.Property("ProfileName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ProfileID", "GameID", "ConfigurationSource"); + + b.HasIndex("GameID"); + + b.HasIndex("ProfileID") + .IsUnique(); + + b.ToTable("GameRecordsConfigurationProfiles"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.PortDeviceEntryModel", b => + { + b.Property("OrchestratorName") + .HasColumnType("TEXT"); + + b.Property("PlatformID") + .HasColumnType("TEXT"); + + b.Property("PortIndex") + .HasColumnType("INTEGER"); + + b.Property("ControllerID") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Driver") + .HasColumnType("INTEGER"); + + b.Property("InstanceGuid") + .HasColumnType("TEXT"); + + b.Property("ProfileGuid") + .HasColumnType("TEXT"); + + b.HasKey("OrchestratorName", "PlatformID", "PortIndex"); + + b.ToTable("PortDeviceEntries"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.RecordMetadataModel", b => + { + b.Property("RecordMetadataID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("MetadataKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MetadataValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RecordID") + .HasColumnType("TEXT"); + + b.HasKey("RecordMetadataID"); + + b.HasIndex("RecordID"); + + b.ToTable("Metadata"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.RecordModel", b => + { + b.Property("RecordID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ContentLibraryLibraryID") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RecordType") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("RecordID"); + + b.HasIndex("ContentLibraryLibraryID"); + + b.ToTable("Records"); + + b.HasDiscriminator("Discriminator").HasValue("RecordModel"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.FileRecordModel", b => + { + b.HasBaseType("Snowflake.Model.Database.Models.RecordModel"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasDiscriminator().HasValue("FileRecordModel"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.GameRecordModel", b => + { + b.HasBaseType("Snowflake.Model.Database.Models.RecordModel"); + + b.Property("PlatformID") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasDiscriminator().HasValue("GameRecordModel"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.ConfigurationValueModel", b => + { + b.HasOne("Snowflake.Model.Database.Models.ConfigurationProfileModel", "Profile") + .WithMany("Values") + .HasForeignKey("ValueCollectionGuid") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.ControllerElementMappingModel", b => + { + b.HasOne("Snowflake.Model.Database.Models.ControllerElementMappingCollectionModel", "Collection") + .WithMany("MappedElements") + .HasForeignKey("ProfileID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.GameRecordConfigurationProfileModel", b => + { + b.HasOne("Snowflake.Model.Database.Models.GameRecordModel", "Game") + .WithMany("ConfigurationProfiles") + .HasForeignKey("GameID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Snowflake.Model.Database.Models.ConfigurationProfileModel", "Profile") + .WithOne() + .HasForeignKey("Snowflake.Model.Database.Models.GameRecordConfigurationProfileModel", "ProfileID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.RecordMetadataModel", b => + { + b.HasOne("Snowflake.Model.Database.Models.RecordModel", "Record") + .WithMany("Metadata") + .HasForeignKey("RecordID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Record"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.RecordModel", b => + { + b.HasOne("Snowflake.Model.Database.Models.ContentLibraryModel", "ContentLibrary") + .WithMany("Records") + .HasForeignKey("ContentLibraryLibraryID"); + + b.Navigation("ContentLibrary"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.ConfigurationProfileModel", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.ContentLibraryModel", b => + { + b.Navigation("Records"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.ControllerElementMappingCollectionModel", b => + { + b.Navigation("MappedElements"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.RecordModel", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.GameRecordModel", b => + { + b.Navigation("ConfigurationProfiles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Snowflake.Framework/Model/Database/Migrations/20221003040212_AddContentLibrary.cs b/src/Snowflake.Framework/Model/Database/Migrations/20221003040212_AddContentLibrary.cs new file mode 100644 index 000000000..e026132b7 --- /dev/null +++ b/src/Snowflake.Framework/Model/Database/Migrations/20221003040212_AddContentLibrary.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snowflake.Model.Database.Migrations +{ + public partial class AddContentLibrary : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ContentLibraryLibraryID", + table: "Records", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateTable( + name: "ContentLibraries", + columns: table => new + { + LibraryID = table.Column(type: "TEXT", nullable: false), + Path = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ContentLibraries", x => x.LibraryID); + }); + + migrationBuilder.CreateIndex( + name: "IX_Records_ContentLibraryLibraryID", + table: "Records", + column: "ContentLibraryLibraryID"); + + migrationBuilder.AddForeignKey( + name: "FK_Records_ContentLibraries_ContentLibraryLibraryID", + table: "Records", + column: "ContentLibraryLibraryID", + principalTable: "ContentLibraries", + principalColumn: "LibraryID"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Records_ContentLibraries_ContentLibraryLibraryID", + table: "Records"); + + migrationBuilder.DropTable( + name: "ContentLibraries"); + + migrationBuilder.DropIndex( + name: "IX_Records_ContentLibraryLibraryID", + table: "Records"); + + migrationBuilder.DropColumn( + name: "ContentLibraryLibraryID", + table: "Records"); + } + } +} diff --git a/src/Snowflake.Framework/Model/Database/Migrations/DatabaseContextModelSnapshot.cs b/src/Snowflake.Framework/Model/Database/Migrations/DatabaseContextModelSnapshot.cs index ca830bc1d..00c055a2b 100644 --- a/src/Snowflake.Framework/Model/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/src/Snowflake.Framework/Model/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Snowflake.Model.Database.Models; +#nullable disable + namespace Snowflake.Model.Database.Migrations { [DbContext(typeof(DatabaseContext))] @@ -13,8 +15,7 @@ partial class DatabaseContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.3"); + modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); modelBuilder.Entity("Snowflake.Model.Database.Models.ConfigurationProfileModel", b => { @@ -61,6 +62,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ConfigurationValues"); }); + modelBuilder.Entity("Snowflake.Model.Database.Models.ContentLibraryModel", b => + { + b.Property("LibraryID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LibraryID"); + + b.ToTable("ContentLibraries"); + }); + modelBuilder.Entity("Snowflake.Model.Database.Models.ControllerElementMappingCollectionModel", b => { b.Property("ProfileID") @@ -190,6 +206,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("TEXT"); + b.Property("ContentLibraryLibraryID") + .HasColumnType("TEXT"); + b.Property("Discriminator") .IsRequired() .HasColumnType("TEXT"); @@ -200,6 +219,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("RecordID"); + b.HasIndex("ContentLibraryLibraryID"); + b.ToTable("Records"); b.HasDiscriminator("Discriminator").HasValue("RecordModel"); @@ -234,6 +255,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("ValueCollectionGuid") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Profile"); }); modelBuilder.Entity("Snowflake.Model.Database.Models.ControllerElementMappingModel", b => @@ -243,6 +266,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("ProfileID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Collection"); }); modelBuilder.Entity("Snowflake.Model.Database.Models.GameRecordConfigurationProfileModel", b => @@ -258,6 +283,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("Snowflake.Model.Database.Models.GameRecordConfigurationProfileModel", "ProfileID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("Profile"); }); modelBuilder.Entity("Snowflake.Model.Database.Models.RecordMetadataModel", b => @@ -267,6 +296,42 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("RecordID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Record"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.RecordModel", b => + { + b.HasOne("Snowflake.Model.Database.Models.ContentLibraryModel", "ContentLibrary") + .WithMany("Records") + .HasForeignKey("ContentLibraryLibraryID"); + + b.Navigation("ContentLibrary"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.ConfigurationProfileModel", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.ContentLibraryModel", b => + { + b.Navigation("Records"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.ControllerElementMappingCollectionModel", b => + { + b.Navigation("MappedElements"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.RecordModel", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("Snowflake.Model.Database.Models.GameRecordModel", b => + { + b.Navigation("ConfigurationProfiles"); }); #pragma warning restore 612, 618 } diff --git a/src/Snowflake.Framework/Model/Database/Models/ContentLibraryModel.cs b/src/Snowflake.Framework/Model/Database/Models/ContentLibraryModel.cs index 13a580e34..c578a7f48 100644 --- a/src/Snowflake.Framework/Model/Database/Models/ContentLibraryModel.cs +++ b/src/Snowflake.Framework/Model/Database/Models/ContentLibraryModel.cs @@ -11,15 +11,22 @@ internal class ContentLibraryModel { public string Path { get; set; } - public Guid LibraryId { get; set; } + public Guid LibraryID { get; set; } + + public List Records { get; set; } + internal static void SetupModel(ModelBuilder modelBuilder) { modelBuilder.Entity() - .HasKey(p => p.LibraryId); + .HasKey(p => p.LibraryID); modelBuilder.Entity() .Property(p => p.Path) .IsRequired(); + + modelBuilder.Entity() + .HasMany(g => g.Records) + .WithOne(p => p.ContentLibrary); } } } diff --git a/src/Snowflake.Framework/Model/Database/Models/RecordModel.cs b/src/Snowflake.Framework/Model/Database/Models/RecordModel.cs index 1123dcc7c..c4c91eb85 100644 --- a/src/Snowflake.Framework/Model/Database/Models/RecordModel.cs +++ b/src/Snowflake.Framework/Model/Database/Models/RecordModel.cs @@ -15,6 +15,8 @@ internal class RecordModel public List Metadata { get; set; } + public ContentLibraryModel ContentLibrary { get; set; } + internal static void SetupModel(ModelBuilder modelBuilder) { modelBuilder.Entity() diff --git a/src/Snowflake.Framework/Model/Game/LibraryExtensions/GameFileExtension.cs b/src/Snowflake.Framework/Model/Game/LibraryExtensions/GameFileExtension.cs index df932f109..9e784c05e 100644 --- a/src/Snowflake.Framework/Model/Game/LibraryExtensions/GameFileExtension.cs +++ b/src/Snowflake.Framework/Model/Game/LibraryExtensions/GameFileExtension.cs @@ -10,9 +10,9 @@ namespace Snowflake.Model.Game.LibraryExtensions { internal class GameFileExtension : IGameFileExtension { - public GameFileExtension(IFileSystem gameFsRoot, FileRecordLibrary files) + public GameFileExtension(IDirectory gameFsRoot, FileRecordLibrary files) { - this._Root = new Directory(gameFsRoot); + this._Root = gameFsRoot; this.FileRecordLibrary = files; this.SavesRoot = this._Root.OpenDirectory("saves"); this.ProgramRoot = this._Root.OpenDirectory("program"); diff --git a/src/Snowflake.Framework/Model/Game/LibraryExtensions/GameFileExtensionProvider.cs b/src/Snowflake.Framework/Model/Game/LibraryExtensions/GameFileExtensionProvider.cs index 392c97758..68ac75324 100644 --- a/src/Snowflake.Framework/Model/Game/LibraryExtensions/GameFileExtensionProvider.cs +++ b/src/Snowflake.Framework/Model/Game/LibraryExtensions/GameFileExtensionProvider.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; using System.Threading.Tasks; +using Snowflake.Filesystem.Library; using Snowflake.Model.Database; using Snowflake.Model.Game.LibraryExtensions; using Snowflake.Model.Records.File; @@ -14,21 +16,24 @@ internal class GameFileExtensionProvider : IGameFileExtensionProvider { public GameFileExtensionProvider (FileRecordLibrary fileLibrary, - IFileSystem gameFolderFs) + ContentLibraryStore contentLibrary) { this.FileLibrary = fileLibrary; - this.GameFolderRoot = gameFolderFs; + this.ContentLibrary = contentLibrary; } internal FileRecordLibrary FileLibrary { get; } - internal IFileSystem GameFolderRoot { get; } - + internal ContentLibraryStore ContentLibrary { get; } public IGameFileExtension MakeExtension(IGameRecord record) { - var gameFsRoot = this.GameFolderRoot - .GetOrCreateSubFileSystem((UPath) "/" / record.RecordID.ToString()); - return new GameFileExtension(gameFsRoot, this.FileLibrary); + var libraryRoot = this.ContentLibrary.GetRecordLibrary(record); + + if (libraryRoot == null) + { + throw new FileNotFoundException("Game is missing associated content library."); + } + return new GameFileExtension(libraryRoot.OpenRecordLibrary(record.RecordID), this.FileLibrary); } public void UpdateFile(IFileRecord file) diff --git a/src/Snowflake.Framework/Model/Records/GameRecord.cs b/src/Snowflake.Framework/Model/Records/GameRecord.cs index 5a8169935..821e0a2ee 100644 --- a/src/Snowflake.Framework/Model/Records/GameRecord.cs +++ b/src/Snowflake.Framework/Model/Records/GameRecord.cs @@ -12,7 +12,8 @@ public class GameRecord : IGameRecord { internal GameRecord(PlatformId platform, Guid recordId, - IMetadataCollection metadata) + IMetadataCollection metadata + ) { this.PlatformID = platform; this.RecordID = recordId; @@ -28,7 +29,6 @@ public string? Title } public IMetadataCollection Metadata { get; } - public Guid RecordID { get; } } } diff --git a/src/Snowflake.Framework/Snowflake - Backup.Framework.csproj b/src/Snowflake.Framework/Snowflake - Backup.Framework.csproj deleted file mode 100644 index a6447e0b4..000000000 --- a/src/Snowflake.Framework/Snowflake - Backup.Framework.csproj +++ /dev/null @@ -1,46 +0,0 @@ - - - - net6.0 - Snowflake - $(OutputPath)$(AssemblyName).xml - 10.0 - enable - <_SnowflakeUseDevelopmentSDK>true - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - ..\Snowflake.ruleset - - - NU1605;8604;8061;8602;8603;8612;8613;8620 - 1701;1702;1705;1519;1591;8619 - - \ No newline at end of file diff --git a/src/Snowflake.Support.GraphQL.FrameworkQueries/Mutations/Game/CreateGameInput.cs b/src/Snowflake.Support.GraphQL.FrameworkQueries/Mutations/Game/CreateGameInput.cs index 98a224f6d..c58f32125 100644 --- a/src/Snowflake.Support.GraphQL.FrameworkQueries/Mutations/Game/CreateGameInput.cs +++ b/src/Snowflake.Support.GraphQL.FrameworkQueries/Mutations/Game/CreateGameInput.cs @@ -12,6 +12,8 @@ internal sealed class CreateGameInput : RelayMutationBase { public PlatformId PlatformID { get; set; } + + public Guid LibraryID { get; set; } } internal sealed class CreateGameInputType @@ -26,6 +28,11 @@ protected override void Configure(IInputObjectTypeDescriptor de .Name("platformId") .Description("The Stone platform ID of the platform of this game.") .Type>(); + + descriptor.Field(i => i.LibraryID) + .Name("libraryId") + .Description("The ID of the content library to store files for this game.") + .Type>(); } } } diff --git a/src/Snowflake.Support.GraphQL.FrameworkQueries/Mutations/Game/GameMutations.cs b/src/Snowflake.Support.GraphQL.FrameworkQueries/Mutations/Game/GameMutations.cs index 4e9a1c431..e2aee5b0d 100644 --- a/src/Snowflake.Support.GraphQL.FrameworkQueries/Mutations/Game/GameMutations.cs +++ b/src/Snowflake.Support.GraphQL.FrameworkQueries/Mutations/Game/GameMutations.cs @@ -1,5 +1,6 @@ using HotChocolate.Subscriptions; using HotChocolate.Types; +using Snowflake.Filesystem.Library; using Snowflake.Model.Game; using Snowflake.Model.Records; using Snowflake.Remoting.GraphQL; @@ -25,9 +26,18 @@ protected override void Configure(IObjectTypeDescriptor descriptor) .Resolve(async ctx => { var gameLibrary = ctx.SnowflakeService(); - + var contextStore = ctx.SnowflakeService(); CreateGameInput args = ctx.ArgumentValue("input"); var game = await gameLibrary.CreateGameAsync(args.PlatformID); + + // todo: expose async + var library = contextStore.GetLibrary(args.LibraryID); + if (library == null) + { + throw new Exception("Content library is unknown"); + } + contextStore.SetRecordLibrary(library, game.Record); + return new GamePayload() { Game = game, diff --git a/src/Snowflake.Support.GraphQL.FrameworkQueries/Queries/Filesystem/FilesystemQueries.cs b/src/Snowflake.Support.GraphQL.FrameworkQueries/Queries/Filesystem/FilesystemQueries.cs index ea47d7fa0..2cee41e59 100644 --- a/src/Snowflake.Support.GraphQL.FrameworkQueries/Queries/Filesystem/FilesystemQueries.cs +++ b/src/Snowflake.Support.GraphQL.FrameworkQueries/Queries/Filesystem/FilesystemQueries.cs @@ -1,5 +1,6 @@ using HotChocolate.Types; using Microsoft.DotNet.PlatformAbstractions; +using Snowflake.Filesystem.Library; using Snowflake.Remoting.GraphQL; using Snowflake.Remoting.GraphQL.Model.Filesystem; using Snowflake.Remoting.GraphQL.Model.Filesystem.Contextual; @@ -37,6 +38,11 @@ protected override void Configure(IObjectTypeDescriptor descriptor) .Type() .Description("Provides normalized OS-dependent filesystem access." + "Returns null if the specified path does not exist."); + + descriptor.Field("contentLibrary") + .Resolve(context => context.SnowflakeService()?.GetLibraries()) + .Type>>() + .Description("Lists the available content libraries."); } } } diff --git a/src/Snowflake.Support.GraphQL.FrameworkQueries/Queries/Game/LibraryExtensions/GameFileQueries.cs b/src/Snowflake.Support.GraphQL.FrameworkQueries/Queries/Game/LibraryExtensions/GameFileQueries.cs index 3acda6d54..0fe7001e3 100644 --- a/src/Snowflake.Support.GraphQL.FrameworkQueries/Queries/Game/LibraryExtensions/GameFileQueries.cs +++ b/src/Snowflake.Support.GraphQL.FrameworkQueries/Queries/Game/LibraryExtensions/GameFileQueries.cs @@ -1,4 +1,4 @@ -using HotChocolate.Types; + using HotChocolate.Types; using Snowflake.Remoting.GraphQL.Model.Game; using Snowflake.Model.Game; using Snowflake.Model.Game.LibraryExtensions; @@ -19,8 +19,18 @@ protected override void Configure(IObjectTypeDescriptor descriptor) descriptor.Field("files") .Type() - .Description("Provides access to the game's files.") - .Resolve(context => context.Parent().WithFiles()); + .Description("Provides access to the game's files. If this game does not have a registered content library, returns null.") + .Resolve(context => + { + try + { + return context.Parent().WithFiles(); + } + catch + { + return null; + } + }); } } } diff --git a/src/Snowflake.Support.StoreProviders/StoreProviderComposable.cs b/src/Snowflake.Support.StoreProviders/StoreProviderComposable.cs index 11db4ab3d..5df4870bd 100644 --- a/src/Snowflake.Support.StoreProviders/StoreProviderComposable.cs +++ b/src/Snowflake.Support.StoreProviders/StoreProviderComposable.cs @@ -52,9 +52,18 @@ public void Compose(IModule module, Loader.IServiceRepository serviceContainer) var appDataPath = physicalFs.ConvertPathFromInternal(contentDirectory.ApplicationData.FullName); var gameFs = physicalFs.GetOrCreateSubFileSystem(appDataPath / "games"); + + + var contentStore = new ContentLibraryStore(physicalFs, optionsBuilder); + serviceContainer.Get() + .RegisterService(contentStore); + + // Create the default directory + contentStore.CreateLibrary(Directory.CreateDirectory(gameFs.ConvertPathToInternal(@"\"))); + // Add default extensions gameLibrary.AddExtension(new GameFileExtensionProvider(fileLibrary, gameFs)); + IGameFileExtension>(new GameFileExtensionProvider(fileLibrary, contentStore)); gameLibrary.AddExtension(new GameConfigurationExtensionProvider(configStore)); @@ -81,9 +90,6 @@ public void Compose(IModule module, Loader.IServiceRepository serviceContainer) serviceContainer.Get() .RegisterService(portStore); - var contentStore = new ContentLibraryStore(physicalFs, optionsBuilder); - serviceContainer.Get() - .RegisterService(contentStore); } } }