diff --git a/Minecraft-Realms-Emulator/Entities/Backup.cs b/Minecraft-Realms-Emulator/Entities/Backup.cs index fc2eb8d..ec6f7cc 100644 --- a/Minecraft-Realms-Emulator/Entities/Backup.cs +++ b/Minecraft-Realms-Emulator/Entities/Backup.cs @@ -5,10 +5,13 @@ namespace Minecraft_Realms_Emulator.Entities public class Backup { public int Id { get; set; } - public World World { get; set; } - public string BackupId { get; set; } + public Slot Slot { get; set; } = null!; + public string BackupId { get; set; } = null!; public long LastModifiedDate { get; set; } public int Size { get; set; } - public JsonDocument Metadata { get; set; } + public JsonDocument Metadata { get; set; } = null!; + public string DownloadUrl { get; set; } = null!; + public string? ResourcePackUrl { get; set; } = null!; + public string? ResourcePackHash { get; set; } = null!; } } diff --git a/Minecraft-Realms-Emulator/Middlewares/RouteLoggingMiddleware.cs b/Minecraft-Realms-Emulator/Middlewares/RouteLoggingMiddleware.cs new file mode 100644 index 0000000..3fe82a3 --- /dev/null +++ b/Minecraft-Realms-Emulator/Middlewares/RouteLoggingMiddleware.cs @@ -0,0 +1,14 @@ +namespace Minecraft_Realms_Emulator.Middlewares +{ + public class RouteLoggingMiddleware(RequestDelegate next) + { + private readonly RequestDelegate _next = next; + + public async Task Invoke(HttpContext httpContext) + { + Console.WriteLine($"{httpContext.Request.Method} {httpContext.Request.Path}{httpContext.Request.QueryString}"); + + await _next(httpContext); + } + } +} diff --git a/Minecraft-Realms-Emulator/Migrations/20240630160411_Backup_Slots.Designer.cs b/Minecraft-Realms-Emulator/Migrations/20240630160411_Backup_Slots.Designer.cs new file mode 100644 index 0000000..0217a1d --- /dev/null +++ b/Minecraft-Realms-Emulator/Migrations/20240630160411_Backup_Slots.Designer.cs @@ -0,0 +1,512 @@ +// +using System; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Minecraft_Realms_Emulator.Data; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Minecraft_Realms_Emulator.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20240630160411_Backup_Slots")] + partial class Backup_Slots + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Backup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BackupId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DownloadUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModifiedDate") + .HasColumnType("bigint"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ResourcePackHash") + .HasColumnType("text"); + + b.Property("ResourcePackUrl") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("integer"); + + b.Property("SlotId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SlotId"); + + b.ToTable("Backups"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Configuration", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Key"); + + b.ToTable("Configuration"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Connection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("PendingUpdate") + .HasColumnType("boolean"); + + b.Property("WorldId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorldId"); + + b.ToTable("Connections"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Invite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("InvitationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipeintUUID") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorldId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorldId"); + + b.ToTable("Invites"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ButtonText") + .HasColumnType("jsonb"); + + b.Property("Dismissable") + .HasColumnType("boolean"); + + b.Property("Image") + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("NotificationUuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("jsonb"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("UrlButton") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Accepted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Online") + .HasColumnType("boolean"); + + b.Property("Operator") + .HasColumnType("boolean"); + + b.Property("Permission") + .IsRequired() + .HasColumnType("text"); + + b.Property("Uuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorldId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorldId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.SeenNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("NotificationUUID") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlayerUUID") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SeenNotifications"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Slot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CommandBlocks") + .HasColumnType("boolean"); + + b.Property("Difficulty") + .HasColumnType("integer"); + + b.Property("ForceGameMode") + .HasColumnType("boolean"); + + b.Property("GameMode") + .HasColumnType("integer"); + + b.Property("Pvp") + .HasColumnType("boolean"); + + b.Property("SlotId") + .HasColumnType("integer"); + + b.Property("SlotName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SpawnAnimals") + .HasColumnType("boolean"); + + b.Property("SpawnMonsters") + .HasColumnType("boolean"); + + b.Property("SpawnNPCs") + .HasColumnType("boolean"); + + b.Property("SpawnProtection") + .HasColumnType("integer"); + + b.Property("Version") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorldId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorldId"); + + b.ToTable("Slots"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriptionType") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorldId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorldId") + .IsUnique(); + + b.ToTable("Subscriptions"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Template", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("Image") + .HasColumnType("text"); + + b.Property("Link") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecommendedPlayers") + .IsRequired() + .HasColumnType("text"); + + b.Property("Trailer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Templates"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.World", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveSlot") + .HasColumnType("integer"); + + b.Property("MaxPlayers") + .HasColumnType("integer"); + + b.Property("Member") + .HasColumnType("boolean"); + + b.Property("MinigameId") + .HasColumnType("integer"); + + b.Property("Motd") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Owner") + .HasColumnType("text"); + + b.Property("OwnerUUID") + .HasColumnType("text"); + + b.Property("ParentWorldId") + .HasColumnType("integer"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorldType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MinigameId"); + + b.HasIndex("ParentWorldId"); + + b.ToTable("Worlds"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Backup", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.Slot", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Slot"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Connection", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.World", "World") + .WithMany() + .HasForeignKey("WorldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("World"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Invite", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.World", "World") + .WithMany() + .HasForeignKey("WorldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("World"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Player", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.World", "World") + .WithMany("Players") + .HasForeignKey("WorldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("World"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Slot", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.World", "World") + .WithMany("Slots") + .HasForeignKey("WorldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("World"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Subscription", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.World", "World") + .WithOne("Subscription") + .HasForeignKey("Minecraft_Realms_Emulator.Entities.Subscription", "WorldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("World"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.World", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.Template", "Minigame") + .WithMany() + .HasForeignKey("MinigameId"); + + b.HasOne("Minecraft_Realms_Emulator.Entities.World", "ParentWorld") + .WithMany() + .HasForeignKey("ParentWorldId"); + + b.Navigation("Minigame"); + + b.Navigation("ParentWorld"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.World", b => + { + b.Navigation("Players"); + + b.Navigation("Slots"); + + b.Navigation("Subscription"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Minecraft-Realms-Emulator/Migrations/20240630160411_Backup_Slots.cs b/Minecraft-Realms-Emulator/Migrations/20240630160411_Backup_Slots.cs new file mode 100644 index 0000000..1dca768 --- /dev/null +++ b/Minecraft-Realms-Emulator/Migrations/20240630160411_Backup_Slots.cs @@ -0,0 +1,93 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Minecraft_Realms_Emulator.Migrations +{ + /// + public partial class Backup_Slots : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Backups_Worlds_WorldId", + table: "Backups"); + + migrationBuilder.RenameColumn( + name: "WorldId", + table: "Backups", + newName: "SlotId"); + + migrationBuilder.RenameIndex( + name: "IX_Backups_WorldId", + table: "Backups", + newName: "IX_Backups_SlotId"); + + migrationBuilder.AddColumn( + name: "DownloadUrl", + table: "Backups", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "ResourcePackHash", + table: "Backups", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "ResourcePackUrl", + table: "Backups", + type: "text", + nullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_Backups_Slots_SlotId", + table: "Backups", + column: "SlotId", + principalTable: "Slots", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Backups_Slots_SlotId", + table: "Backups"); + + migrationBuilder.DropColumn( + name: "DownloadUrl", + table: "Backups"); + + migrationBuilder.DropColumn( + name: "ResourcePackHash", + table: "Backups"); + + migrationBuilder.DropColumn( + name: "ResourcePackUrl", + table: "Backups"); + + migrationBuilder.RenameColumn( + name: "SlotId", + table: "Backups", + newName: "WorldId"); + + migrationBuilder.RenameIndex( + name: "IX_Backups_SlotId", + table: "Backups", + newName: "IX_Backups_WorldId"); + + migrationBuilder.AddForeignKey( + name: "FK_Backups_Worlds_WorldId", + table: "Backups", + column: "WorldId", + principalTable: "Worlds", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Minecraft-Realms-Emulator/Migrations/DataContextModelSnapshot.cs b/Minecraft-Realms-Emulator/Migrations/DataContextModelSnapshot.cs index 39cf6fb..c6b1830 100644 --- a/Minecraft-Realms-Emulator/Migrations/DataContextModelSnapshot.cs +++ b/Minecraft-Realms-Emulator/Migrations/DataContextModelSnapshot.cs @@ -35,6 +35,10 @@ namespace Minecraft_Realms_Emulator.Migrations .IsRequired() .HasColumnType("text"); + b.Property("DownloadUrl") + .IsRequired() + .HasColumnType("text"); + b.Property("LastModifiedDate") .HasColumnType("bigint"); @@ -42,15 +46,21 @@ namespace Minecraft_Realms_Emulator.Migrations .IsRequired() .HasColumnType("jsonb"); + b.Property("ResourcePackHash") + .HasColumnType("text"); + + b.Property("ResourcePackUrl") + .HasColumnType("text"); + b.Property("Size") .HasColumnType("integer"); - b.Property("WorldId") + b.Property("SlotId") .HasColumnType("integer"); b.HasKey("Id"); - b.HasIndex("WorldId"); + b.HasIndex("SlotId"); b.ToTable("Backups"); }); @@ -406,13 +416,13 @@ namespace Minecraft_Realms_Emulator.Migrations modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Backup", b => { - b.HasOne("Minecraft_Realms_Emulator.Entities.World", "World") + b.HasOne("Minecraft_Realms_Emulator.Entities.Slot", "Slot") .WithMany() - .HasForeignKey("WorldId") + .HasForeignKey("SlotId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("World"); + b.Navigation("Slot"); }); modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Connection", b => diff --git a/Minecraft-Realms-Emulator/Modes/External/WorldsController.cs b/Minecraft-Realms-Emulator/Modes/External/WorldsController.cs index c62331e..fc0d7b4 100644 --- a/Minecraft-Realms-Emulator/Modes/External/WorldsController.cs +++ b/Minecraft-Realms-Emulator/Modes/External/WorldsController.cs @@ -784,7 +784,7 @@ namespace Minecraft_Realms_Emulator.Modes.External [CheckRealmOwner] public async Task> GetBackups(int wId) { - var backups = await _context.Backups.Where(b => b.World.Id == wId).ToListAsync(); + var backups = await _context.Backups.Where(b => b.Slot.World.Id == wId).ToListAsync(); BackupsResponse worldBackups = new() { @@ -794,6 +794,34 @@ namespace Minecraft_Realms_Emulator.Modes.External return Ok(worldBackups); } + [HttpGet("{wId}/slot/{sId}/download")] + [CheckForWorld] + [CheckRealmOwner] + public ActionResult GetBackup(int wId, int sId) + { + Backup backup = _context.Backups.Include(b => b.Slot).FirstOrDefault(b => b.Slot.World.Id == wId && b.Slot.Id == sId); + + if (backup == null) + { + ErrorResponse errorResponse = new() + { + ErrorCode = 404, + ErrorMsg = "No backup found" + }; + + return NotFound(errorResponse); + } + + BackupDownloadResponse backupDownloadResponse = new() + { + DownloadLink = backup.DownloadUrl, + ResourcePackUrl = backup.ResourcePackUrl, + ResourcePackHash = backup.ResourcePackHash, + }; + + return Ok(backupDownloadResponse); + } + [HttpGet("v1/{wId}/join/pc")] public ActionResult Join(int wId) { diff --git a/Minecraft-Realms-Emulator/Modes/Realms/Controllers/WorldsController.cs b/Minecraft-Realms-Emulator/Modes/Realms/Controllers/WorldsController.cs index b97245d..c588cdf 100644 --- a/Minecraft-Realms-Emulator/Modes/Realms/Controllers/WorldsController.cs +++ b/Minecraft-Realms-Emulator/Modes/Realms/Controllers/WorldsController.cs @@ -855,7 +855,7 @@ namespace Minecraft_Realms_Emulator.Modes.Realms.Controllers [CheckRealmOwner] public async Task> GetBackups(int wId) { - var backups = await _context.Backups.Where(b => b.World.Id == wId).ToListAsync(); + var backups = await _context.Backups.Where(b => b.Slot.World.Id == wId).ToListAsync(); BackupsResponse worldBackups = new() { @@ -865,6 +865,34 @@ namespace Minecraft_Realms_Emulator.Modes.Realms.Controllers return Ok(worldBackups); } + [HttpGet("{wId}/slot/{sId}/download")] + [CheckForWorld] + [CheckRealmOwner] + public ActionResult GetBackup(int wId, int sId) + { + Backup backup = _context.Backups.Include(b => b.Slot).FirstOrDefault(b => b.Slot.World.Id == wId && b.Slot.Id == sId); + + if (backup == null) + { + ErrorResponse errorResponse = new() + { + ErrorCode = 404, + ErrorMsg = "No backup found" + }; + + return NotFound(errorResponse); + } + + BackupDownloadResponse backupDownloadResponse = new() + { + DownloadLink = backup.DownloadUrl, + ResourcePackUrl = backup.ResourcePackUrl, + ResourcePackHash = backup.ResourcePackHash, + }; + + return Ok(backupDownloadResponse); + } + [HttpGet("v1/{wId}/join/pc")] public ActionResult Join(int wId) { diff --git a/Minecraft-Realms-Emulator/Program.cs b/Minecraft-Realms-Emulator/Program.cs index 41bda87..c8f197d 100644 --- a/Minecraft-Realms-Emulator/Program.cs +++ b/Minecraft-Realms-Emulator/Program.cs @@ -125,6 +125,7 @@ app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); +app.UseMiddleware(); Console.WriteLine($"Running in {mode.Value} mode"); app.Run(); diff --git a/Minecraft-Realms-Emulator/Responses/BackupDownloadResponse.cs b/Minecraft-Realms-Emulator/Responses/BackupDownloadResponse.cs new file mode 100644 index 0000000..2146398 --- /dev/null +++ b/Minecraft-Realms-Emulator/Responses/BackupDownloadResponse.cs @@ -0,0 +1,9 @@ +namespace Minecraft_Realms_Emulator.Responses +{ + public class BackupDownloadResponse + { + public string DownloadLink { get; set; } = null!; + public string? ResourcePackUrl { get; set; } + public string? ResourcePackHash { get; set; } + } +}