diff --git a/Dockerfile b/Dockerfile index e6227f6b..965ce810 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,4 +37,4 @@ RUN chown -R lighthouse:lighthouse /lighthouse && \ chmod +x /lighthouse/docker-entrypoint.sh && \ cp /lighthouse/app/appsettings.json /lighthouse/temp -ENTRYPOINT ["/lighthouse/docker-entrypoint.sh"] +ENTRYPOINT ["/lighthouse/docker-entrypoint.sh"] \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ClientConfigurationController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ClientConfigurationController.cs index a305c623..00fb8505 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ClientConfigurationController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ClientConfigurationController.cs @@ -54,9 +54,7 @@ public class ClientConfigurationController : ControllerBase [Produces("text/xml")] public async Task GetPrivacySettings() { - GameToken token = this.GetToken(); - - User? user = await this.database.UserFromGameToken(token); + User? user = await this.database.UserFromGameToken(this.GetToken()); if (user == null) return this.StatusCode(403, ""); PrivacySettings ps = new() @@ -72,7 +70,7 @@ public class ClientConfigurationController : ControllerBase [Produces("text/xml")] public async Task SetPrivacySetting() { - User? user = await this.database.UserFromGameRequest(this.Request); + User? user = await this.database.UserFromGameToken(this.GetToken()); if (user == null) return this.StatusCode(403, ""); PrivacySettings? settings = await this.DeserializeBody(); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs index 7d122e8b..0ffebd33 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs @@ -95,38 +95,40 @@ public class PhotosController : ControllerBase if (validLevel) photo.SlotId = photo.XmlLevelInfo.SlotId; } - if (photo.Subjects.Count > 4) return this.BadRequest(); + if (photo.XmlSubjects?.Count > 4) return this.BadRequest(); if (photo.Timestamp > TimeHelper.Timestamp) photo.Timestamp = TimeHelper.Timestamp; - // Check for duplicate photo subjects - List subjectUserIds = new(4); - foreach (PhotoSubject subject in photo.Subjects) - { - if (subjectUserIds.Contains(subject.Username) && !string.IsNullOrEmpty(subject.Username)) return this.BadRequest(); - - subjectUserIds.Add(subject.Username); - } - - foreach (PhotoSubject subject in photo.Subjects.Where(subject => !string.IsNullOrEmpty(subject.Username))) - { - subject.User = await this.database.Users.FirstOrDefaultAsync(u => u.Username == subject.Username); - - if (subject.User == null) continue; - - subject.UserId = subject.User.UserId; - Logger.Debug($"Adding PhotoSubject (userid {subject.UserId}) to db", LogArea.Photos); - - this.database.PhotoSubjects.Add(subject); - } + this.database.Photos.Add(photo); + // Save to get photo ID for the PhotoSubject foreign keys await this.database.SaveChangesAsync(); - photo.PhotoSubjectIds = photo.Subjects.Where(s => s.UserId != 0).Select(subject => subject.PhotoSubjectId.ToString()).ToArray(); + if (photo.XmlSubjects != null) + { + // Check for duplicate photo subjects + List subjectUserIds = new(4); + foreach (PhotoSubject subject in photo.PhotoSubjects) + { + if (subjectUserIds.Contains(subject.Username) && !string.IsNullOrEmpty(subject.Username)) + return this.BadRequest(); - Logger.Debug($"Adding PhotoSubjectCollection ({photo.PhotoSubjectCollection}) to photo", LogArea.Photos); + subjectUserIds.Add(subject.Username); + } - this.database.Photos.Add(photo); + foreach (PhotoSubject subject in photo.XmlSubjects.Where(subject => !string.IsNullOrEmpty(subject.Username))) + { + subject.User = await this.database.Users.FirstOrDefaultAsync(u => u.Username == subject.Username); + + if (subject.User == null) continue; + + subject.UserId = subject.User.UserId; + subject.PhotoId = photo.PhotoId; + Logger.Debug($"Adding PhotoSubject (userid {subject.UserId}) to db", LogArea.Photos); + + this.database.PhotoSubjects.Add(subject); + } + } await this.database.SaveChangesAsync(); @@ -154,6 +156,8 @@ public class PhotosController : ControllerBase if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer); List photos = await this.database.Photos.Include(p => p.Creator) + .Include(p => p.PhotoSubjects) + .ThenInclude(ps => ps.User) .Where(p => p.SlotId == id) .OrderByDescending(s => s.Timestamp) .Skip(Math.Max(0, pageStart - 1)) @@ -171,8 +175,9 @@ public class PhotosController : ControllerBase int targetUserId = await this.database.UserIdFromUsername(user); if (targetUserId == 0) return this.NotFound(); - List photos = await this.database.Photos.Include - (p => p.Creator) + List photos = await this.database.Photos.Include(p => p.Creator) + .Include(p => p.PhotoSubjects) + .ThenInclude(ps => ps.User) .Where(p => p.CreatorId == targetUserId) .OrderByDescending(s => s.Timestamp) .Skip(Math.Max(0, pageStart - 1)) @@ -190,17 +195,15 @@ public class PhotosController : ControllerBase int targetUserId = await this.database.UserIdFromUsername(user); if (targetUserId == 0) return this.NotFound(); - List photoSubjectIds = new(); - photoSubjectIds.AddRange(this.database.PhotoSubjects.Where(p => p.UserId == targetUserId).Select(p => p.PhotoSubjectId)); - List photos = (from id in photoSubjectIds from p in - this.database.Photos.Include(p => p.Creator).Where(p => p.PhotoSubjectCollection.Contains(id.ToString())) - where p.PhotoSubjectCollection.Split(",").Contains(id.ToString()) && p.CreatorId != targetUserId select p).ToList(); - - string response = photos + List photos = await this.database.Photos.Include(p => p.Creator) + .Include(p => p.PhotoSubjects) + .ThenInclude(ps => ps.User) + .Where(p => p.PhotoSubjects.Any(ps => ps.UserId == targetUserId)) .OrderByDescending(s => s.Timestamp) .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)).Aggregate(string.Empty, - (current, photo) => current + photo.Serialize()); + .Take(Math.Min(pageSize, 30)) + .ToListAsync(); + string response = photos.Aggregate(string.Empty, (current, photo) => current + photo.Serialize()); return this.Ok(LbpSerializer.StringElement("photos", response)); } @@ -219,14 +222,6 @@ public class PhotosController : ControllerBase Slot? photoSlot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == photo.SlotId && s.Type == SlotType.User); if (photoSlot == null || photoSlot.CreatorId != token.UserId) return this.StatusCode(401, ""); } - foreach (string idStr in photo.PhotoSubjectIds) - { - if (string.IsNullOrWhiteSpace(idStr)) continue; - - if (!int.TryParse(idStr, out int subjectId)) continue; - - this.database.PhotoSubjects.RemoveWhere(p => p.PhotoSubjectId == subjectId); - } HashSet photoResources = new(){photo.LargeHash, photo.SmallHash, photo.MediumHash, photo.PlanHash,}; foreach (string hash in photoResources) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs index 1a51a93e..746252eb 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs @@ -48,7 +48,7 @@ public class ReviewController : ControllerBase this.database.RatedLevels.Add(ratedLevel); } - ratedLevel.RatingLBP1 = Math.Max(Math.Min(5, rating), 0); + ratedLevel.RatingLBP1 = Math.Clamp(rating, 0, 5); await this.database.SaveChangesAsync(); diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml index d65eb46d..695fc5b7 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml @@ -76,21 +76,21 @@

-@if (Model.Subjects.Count > 0) +@if (Model.PhotoSubjects.Count > 0) {

- Photo contains @Model.Subjects.Count @(Model.Subjects.Count == 1 ? "person" : "people"): + Photo contains @Model.PhotoSubjects.Count @(Model.PhotoSubjects.Count == 1 ? "person" : "people"):

}
- @foreach (PhotoSubject subject in Model.Subjects) + @foreach (PhotoSubject subject in Model.PhotoSubjects) { @await subject.User.ToLink(Html, ViewData, language, timeZone) }
@{ - PhotoSubject[] subjects = Model.Subjects.ToArray(); + PhotoSubject[] subjects = Model.PhotoSubjects.ToArray(); foreach (PhotoSubject subject in subjects) subject.Username = subject.User.Username; } diff --git a/ProjectLighthouse.Servers.Website/Pages/PhotosPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/PhotosPage.cshtml.cs index 6f9504db..f121b09a 100644 --- a/ProjectLighthouse.Servers.Website/Pages/PhotosPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/PhotosPage.cshtml.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Text; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; @@ -27,22 +28,39 @@ public class PhotosPage : BaseLayout { if (string.IsNullOrWhiteSpace(name)) name = ""; - this.SearchValue = name.Replace(" ", string.Empty); + IQueryable photos = this.Database.Photos.Include(p => p.Creator) + .Include(p => p.PhotoSubjects) + .ThenInclude(ps => ps.User); - this.PhotoCount = await this.Database.Photos.Include - (p => p.Creator) - .CountAsync(p => p.Creator!.Username.Contains(this.SearchValue) || p.PhotoSubjectCollection.Contains(this.SearchValue)); + if (name.Contains("by:") || name.Contains("with:")) + { + foreach (string part in name.Split(" ", StringSplitOptions.RemoveEmptyEntries)) + { + if (part.Contains("by:")) + { + photos = photos.Where(p => p.Creator != null && p.Creator.Username.Contains(part.Replace("by:", ""))); + } + else if (part.Contains("with:")) + { + photos = photos.Where(p => p.PhotoSubjects.Any(ps => ps.User.Username.Contains(part.Replace("with:", "")))); + } + } + } + else + { + photos = photos.Where(p => p.Creator != null && (p.PhotoSubjects.Any(ps => ps.User.Username.Contains(name)) || p.Creator.Username.Contains(name))); + } + + this.SearchValue = name.Trim(); + + this.PhotoCount = await photos.CountAsync(); this.PageNumber = pageNumber; this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.PhotoCount / ServerStatics.PageSize)); if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/photos/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}"); - this.Photos = await this.Database.Photos.Include - (p => p.Creator) - .Include(p => p.Slot) - .Where(p => p.Creator!.Username.Contains(this.SearchValue) || p.PhotoSubjectCollection.Contains(this.SearchValue)) - .OrderByDescending(p => p.Timestamp) + this.Photos = await photos.OrderByDescending(p => p.Timestamp) .Skip(pageNumber * ServerStatics.PageSize) .Take(ServerStatics.PageSize) .ToListAsync(); diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs index 5072e278..f7a6f958 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs @@ -99,6 +99,8 @@ public class SlotPage : BaseLayout } this.Photos = await this.Database.Photos.Include(p => p.Creator) + .Include(p => p.PhotoSubjects) + .ThenInclude(ps => ps.User) .OrderByDescending(p => p.Timestamp) .Where(r => r.SlotId == id) .Take(10) diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotsPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/SlotsPage.cshtml.cs index 2c15fde7..4951f7bf 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SlotsPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/SlotsPage.cshtml.cs @@ -57,26 +57,22 @@ public class SlotsPage : BaseLayout string trimmedSearch = finalSearch.ToString().Trim(); - this.SlotCount = await this.Database.Slots.Include(p => p.Creator) + IQueryable slots = this.Database.Slots.Include(p => p.Creator) .Where(p => p.Type == SlotType.User && !p.Hidden) .Where(p => p.Name.Contains(trimmedSearch)) .Where(p => p.Creator != null && (targetAuthor == null || string.Equals(p.Creator.Username.ToLower(), targetAuthor.ToLower()))) .Where(p => p.Creator != null && (!p.SubLevel || p.Creator == this.User)) - .Where(p => targetGame == null || p.GameVersion == targetGame) - .CountAsync(); + .Where(p => targetGame == null || p.GameVersion == targetGame); + + this.SlotCount = await slots.CountAsync(); this.PageNumber = pageNumber; this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.SlotCount / ServerStatics.PageSize)); if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/slots/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}"); - this.Slots = await this.Database.Slots.Include(p => p.Creator) - .Where(p => p.Type == SlotType.User && !p.Hidden) - .Where(p => p.Name.Contains(trimmedSearch)) - .Where(p => p.Creator != null && (targetAuthor == null || string.Equals(p.Creator.Username.ToLower(), targetAuthor.ToLower()))) - .Where(p => p.Creator != null && (!p.SubLevel || p.Creator == this.User)) + this.Slots = await slots .Where(p => p.Creator!.LevelVisibility == PrivacyType.All) // TODO: change check for when user is logged in - .Where(p => targetGame == null || p.GameVersion == targetGame) .OrderByDescending(p => p.FirstUploaded) .Skip(pageNumber * ServerStatics.PageSize) .Take(ServerStatics.PageSize) diff --git a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs index 17cfbb1c..1a2d39f9 100644 --- a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs @@ -60,6 +60,8 @@ public class UserPage : BaseLayout } this.Photos = await this.Database.Photos.Include(p => p.Slot) + .Include(p => p.PhotoSubjects) + .ThenInclude(ps => ps.User) .OrderByDescending(p => p.Timestamp) .Where(p => p.CreatorId == userId) .Take(6) diff --git a/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupBrokenPhotosMaintenanceJob.cs b/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupBrokenPhotosMaintenanceJob.cs index ca37856b..7128d120 100644 --- a/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupBrokenPhotosMaintenanceJob.cs +++ b/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupBrokenPhotosMaintenanceJob.cs @@ -32,7 +32,7 @@ public class CleanupBrokenPhotosMaintenanceJob : IMaintenanceJob // Checks should generally be ordered in least computationally expensive to most. - if (photo.Subjects.Count > 4) + if (photo.PhotoSubjects.Count > 4) { tooManyPhotoSubjects = true; goto removePhoto; @@ -60,7 +60,7 @@ public class CleanupBrokenPhotosMaintenanceJob : IMaintenanceJob }; List subjectUserIds = new(4); - foreach (PhotoSubject subject in photo.Subjects) + foreach (PhotoSubject subject in photo.PhotoSubjects) { if (subjectUserIds.Contains(subject.UserId)) { diff --git a/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupUnusedPhotoSubjects.cs b/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupUnusedPhotoSubjects.cs deleted file mode 100644 index 292dcd2e..00000000 --- a/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupUnusedPhotoSubjects.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Maintenance; - -namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.MaintenanceJobs; - -public class CleanupUnusedPhotoSubjects : IMaintenanceJob -{ - private readonly DatabaseContext database = new(); - public string Name() => "Cleanup Unused PhotoSubjects"; - public string Description() => "Cleanup unused photo subjects in the database."; - - public async Task Run() - { - List subjectCollections = new(); - List usedPhotoSubjectIds = new(); - - subjectCollections.AddRange(this.database.Photos.Select(p => p.PhotoSubjectCollection)); - - foreach (string idCollection in subjectCollections) - { - usedPhotoSubjectIds.AddRange(idCollection.Split(",").Where(x => int.TryParse(x, out _)).Select(int.Parse)); - } - - IQueryable subjectsToRemove = this.database.PhotoSubjects.Where(p => !usedPhotoSubjectIds.Contains(p.PhotoSubjectId)); - - foreach (PhotoSubject subject in subjectsToRemove) - { - Console.WriteLine(@"Removing subject " + subject.PhotoSubjectId); - this.database.PhotoSubjects.Remove(subject); - } - - await this.database.SaveChangesAsync(); - } - -} \ No newline at end of file diff --git a/ProjectLighthouse/Configuration/ConfigurationBase.cs b/ProjectLighthouse/Configuration/ConfigurationBase.cs index a2424bdc..bd082cc7 100644 --- a/ProjectLighthouse/Configuration/ConfigurationBase.cs +++ b/ProjectLighthouse/Configuration/ConfigurationBase.cs @@ -117,7 +117,7 @@ public abstract class ConfigurationBase where T : class, new() { int newVersion = GetVersion(); Logger.Info($"Upgrading config file from version {storedConfig.ConfigVersion} to version {newVersion}", LogArea.Config); - storedConfig.writeConfig(this.ConfigName + ".bak"); + File.Copy(this.ConfigName, this.ConfigName + "." + GetVersion()); this.loadConfig(storedConfig); this.ConfigVersion = newVersion; this.writeConfig(this.ConfigName); diff --git a/ProjectLighthouse/Database/DatabaseGameTokens.cs b/ProjectLighthouse/Database/DatabaseGameTokens.cs index 46c44bb9..4fde15dd 100644 --- a/ProjectLighthouse/Database/DatabaseGameTokens.cs +++ b/ProjectLighthouse/Database/DatabaseGameTokens.cs @@ -23,28 +23,7 @@ public partial class DatabaseContext { if (token == null) return null; - return await this.Users.FirstOrDefaultAsync(u => u.UserId == token.UserId); - } - - private async Task UserFromMMAuth(string authToken) - { - GameToken? token = await this.GameTokens.FirstOrDefaultAsync(t => t.UserToken == authToken); - - if (token == null) return null; - - if (DateTime.Now <= token.ExpiresAt) return await this.Users.FirstOrDefaultAsync(u => u.UserId == token.UserId); - - this.Remove(token); - await this.SaveChangesAsync(); - - return null; - } - - public async Task UserFromGameRequest(HttpRequest request) - { - if (!request.Cookies.TryGetValue("MM_AUTH", out string? mmAuth)) return null; - - return await this.UserFromMMAuth(mmAuth); + return await this.Users.FindAsync(token.UserId); } public async Task GameTokenFromRequest(HttpRequest request) diff --git a/ProjectLighthouse/Migrations/20230221215252_FixPhotoAndSubjectRelation.cs b/ProjectLighthouse/Migrations/20230221215252_FixPhotoAndSubjectRelation.cs new file mode 100644 index 00000000..4d81fa19 --- /dev/null +++ b/ProjectLighthouse/Migrations/20230221215252_FixPhotoAndSubjectRelation.cs @@ -0,0 +1,57 @@ +using LBPUnion.ProjectLighthouse.Database; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20230221215252_FixPhotoAndSubjectRelation")] + public partial class FixPhotoAndSubjectRelation : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PhotoId", + table: "PhotoSubjects", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.Sql( + "UPDATE PhotoSubjects as ps inner join Photos as p on find_in_set(ps.PhotoSubjectId, p.PhotoSubjectCollection) SET ps.PhotoId = p.PhotoId"); + + // Delete unused PhotoSubjects otherwise foreign key constraint will fail + migrationBuilder.Sql("DELETE from PhotoSubjects where PhotoId = 0"); + + migrationBuilder.CreateIndex( + name: "IX_PhotoSubjects_PhotoId", + table: "PhotoSubjects", + column: "PhotoId"); + + migrationBuilder.AddForeignKey( + name: "FK_PhotoSubjects_Photos_PhotoId", + table: "PhotoSubjects", + column: "PhotoId", + principalTable: "Photos", + principalColumn: "PhotoId", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_PhotoSubjects_Photos_PhotoId", + table: "PhotoSubjects"); + + migrationBuilder.DropIndex( + name: "IX_PhotoSubjects_PhotoId", + table: "PhotoSubjects"); + + migrationBuilder.DropColumn( + name: "PhotoId", + table: "PhotoSubjects"); + } + } +} diff --git a/ProjectLighthouse/Migrations/20230222065412_RemovePhotoSubjectCollection.cs b/ProjectLighthouse/Migrations/20230222065412_RemovePhotoSubjectCollection.cs new file mode 100644 index 00000000..dfa0e171 --- /dev/null +++ b/ProjectLighthouse/Migrations/20230222065412_RemovePhotoSubjectCollection.cs @@ -0,0 +1,31 @@ +using LBPUnion.ProjectLighthouse.Database; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20230222065412_RemovePhotoSubjectCollection")] + public partial class RemovePhotoSubjectCollection : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PhotoSubjectCollection", + table: "Photos"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PhotoSubjectCollection", + table: "Photos", + type: "longtext", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs b/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs index eda06ee2..bdde52e7 100644 --- a/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs +++ b/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs @@ -1,6 +1,5 @@ // using System; -using LBPUnion.ProjectLighthouse; using LBPUnion.ProjectLighthouse.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -683,10 +682,6 @@ namespace ProjectLighthouse.Migrations .IsRequired() .HasColumnType("longtext"); - b.Property("PhotoSubjectCollection") - .IsRequired() - .HasColumnType("longtext"); - b.Property("PlanHash") .IsRequired() .HasColumnType("longtext"); @@ -719,11 +714,16 @@ namespace ProjectLighthouse.Migrations b.Property("Bounds") .HasColumnType("longtext"); + b.Property("PhotoId") + .HasColumnType("int"); + b.Property("UserId") .HasColumnType("int"); b.HasKey("PhotoSubjectId"); + b.HasIndex("PhotoId"); + b.HasIndex("UserId"); b.ToTable("PhotoSubjects"); @@ -1296,12 +1296,20 @@ namespace ProjectLighthouse.Migrations modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoSubject", b => { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.Photo", "Photo") + .WithMany("PhotoSubjects") + .HasForeignKey("PhotoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("Photo"); + b.Navigation("User"); }); @@ -1348,6 +1356,11 @@ namespace ProjectLighthouse.Migrations b.Navigation("User"); }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.Photo", b => + { + b.Navigation("PhotoSubjects"); + }); #pragma warning restore 612, 618 } } diff --git a/ProjectLighthouse/StartupTasks.cs b/ProjectLighthouse/StartupTasks.cs index 124b3e26..ce45ac6f 100644 --- a/ProjectLighthouse/StartupTasks.cs +++ b/ProjectLighthouse/StartupTasks.cs @@ -153,6 +153,8 @@ public static class StartupTasks private static async Task migrateDatabase(DatabaseContext database) { + int? originalTimeout = database.Database.GetCommandTimeout(); + database.Database.SetCommandTimeout(TimeSpan.FromMinutes(5)); // This mutex is used to synchronize migrations across the GameServer, Website, and Api // Without it, each server would try to simultaneously migrate the database resulting in undefined behavior // It is only used for startup and immediately disposed after migrating @@ -189,5 +191,6 @@ public static class StartupTasks Logger.Success($"Extra migration tasks took {stopwatch.ElapsedMilliseconds}ms.", LogArea.Database); Logger.Success($"Total migration took {totalStopwatch.ElapsedMilliseconds}ms.", LogArea.Database); } + database.Database.SetCommandTimeout(originalTimeout); } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Profile/Photo.cs b/ProjectLighthouse/Types/Entities/Profile/Photo.cs index 80e20170..10651d3e 100644 --- a/ProjectLighthouse/Types/Entities/Profile/Photo.cs +++ b/ProjectLighthouse/Types/Entities/Profile/Photo.cs @@ -1,5 +1,4 @@ #nullable enable -using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -9,7 +8,6 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Levels; -using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Types.Entities.Profile; @@ -34,9 +32,6 @@ public class PhotoSlot public class Photo { - [NotMapped] - private List? _subjects; - [NotMapped] [XmlElement("slot")] public PhotoSlot? XmlLevelInfo; @@ -44,7 +39,7 @@ public class Photo [NotMapped] [XmlArray("subjects")] [XmlArrayItem("subject")] - public List? SubjectsXmlDontUseLiterallyEver; + public List? XmlSubjects; [Key] public int PhotoId { get; set; } @@ -65,40 +60,7 @@ public class Photo [XmlElement("plan")] public string PlanHash { get; set; } = ""; - [NotMapped] - public List Subjects { - get { - if (this.SubjectsXmlDontUseLiterallyEver != null) return this.SubjectsXmlDontUseLiterallyEver; - if (this._subjects != null) return this._subjects; - - List response = new(); - using DatabaseContext database = new(); - - foreach (string idStr in this.PhotoSubjectIds.Where(idStr => !string.IsNullOrEmpty(idStr))) - { - if (!int.TryParse(idStr, out int id)) throw new InvalidCastException(idStr + " is not a valid number."); - - PhotoSubject? photoSubject = database.PhotoSubjects - .Include(p => p.User) - .FirstOrDefault(p => p.PhotoSubjectId == id); - if (photoSubject == null) continue; - - response.Add(photoSubject); - } - - return response; - } - set => this._subjects = value; - } - - [NotMapped] - [XmlIgnore] - public string[] PhotoSubjectIds { - get => this.PhotoSubjectCollection.Split(","); - set => this.PhotoSubjectCollection = string.Join(',', value); - } - - public string PhotoSubjectCollection { get; set; } = ""; + public virtual ICollection PhotoSubjects { get; set; } = new HashSet(); public int CreatorId { get; set; } @@ -134,7 +96,7 @@ public class Photo string slot = LbpSerializer.TaggedStringElement("slot", LbpSerializer.StringElement("id", slotId), "type", slotType.ToString().ToLower()); if (slotId == 0) slot = ""; - string subjectsAggregate = this.Subjects.Aggregate(string.Empty, (s, subject) => s + subject.Serialize()); + string subjectsAggregate = this.PhotoSubjects.Aggregate(string.Empty, (s, subject) => s + subject.Serialize()); string photo = LbpSerializer.StringElement("id", this.PhotoId) + LbpSerializer.StringElement("small", this.SmallHash) + diff --git a/ProjectLighthouse/Types/Entities/Profile/PhotoSubject.cs b/ProjectLighthouse/Types/Entities/Profile/PhotoSubject.cs index 93182b6f..ab2e9dc4 100644 --- a/ProjectLighthouse/Types/Entities/Profile/PhotoSubject.cs +++ b/ProjectLighthouse/Types/Entities/Profile/PhotoSubject.cs @@ -7,7 +7,6 @@ using LBPUnion.ProjectLighthouse.Serialization; namespace LBPUnion.ProjectLighthouse.Types.Entities.Profile; -// [XmlRoot("subject")] [XmlType("subject")] [Serializable] public class PhotoSubject @@ -20,10 +19,18 @@ public class PhotoSubject public int UserId { get; set; } [XmlIgnore] - [ForeignKey(nameof(UserId))] [JsonIgnore] + [ForeignKey(nameof(UserId))] public User User { get; set; } + [XmlIgnore] + public int PhotoId { get; set; } + + [XmlIgnore] + [JsonIgnore] + [ForeignKey(nameof(PhotoId))] + public Photo Photo { get; set; } + [NotMapped] [XmlElement("npHandle")] public string Username { get; set; } diff --git a/ProjectLighthouse/Types/Entities/Profile/User.cs b/ProjectLighthouse/Types/Entities/Profile/User.cs index 26888f6a..cb7d097d 100644 --- a/ProjectLighthouse/Types/Entities/Profile/User.cs +++ b/ProjectLighthouse/Types/Entities/Profile/User.cs @@ -10,6 +10,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Types.Misc; using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Types.Entities.Profile; @@ -85,18 +86,9 @@ public class User [JsonIgnore] public int PhotosByMe => this.database.Photos.Count(p => p.CreatorId == this.UserId); - private int PhotosWithMe() - { - List photoSubjectIds = new(); - photoSubjectIds.AddRange(this.database.PhotoSubjects.Where(p => p.UserId == this.UserId) - .Select(p => p.PhotoSubjectId)); - - return ( - from id in photoSubjectIds - from photo in this.database.Photos.Where(p => p.PhotoSubjectCollection.Contains(id.ToString())).ToList() - where photo.PhotoSubjectCollection.Split(",").Contains(id.ToString()) && photo.CreatorId != this.UserId - select id).Count(); - } + [NotMapped] + [JsonIgnore] + public int PhotosWithMe => this.database.Photos.Include(p => p.PhotoSubjects).Count(p => p.PhotoSubjects.Any(ps => ps.UserId == this.UserId)); /// /// The location of the profile card on the user's earth @@ -243,7 +235,7 @@ public class User LbpSerializer.StringElement("reviewCount", this.Reviews, true) + LbpSerializer.StringElement("commentCount", this.Comments, true) + LbpSerializer.StringElement("photosByMeCount", this.PhotosByMe, true) + - LbpSerializer.StringElement("photosWithMeCount", this.PhotosWithMe(), true) + + LbpSerializer.StringElement("photosWithMeCount", this.PhotosWithMe, true) + LbpSerializer.StringElement("commentsEnabled", ServerConfiguration.Instance.UserGeneratedContentLimits.ProfileCommentsEnabled && this.CommentsEnabled) + LbpSerializer.StringElement("location", this.Location.Serialize()) + LbpSerializer.StringElement("favouriteSlotCount", this.HeartedLevels, true) + @@ -284,12 +276,10 @@ public class User [JsonIgnore] public int UsedSlots => this.database.Slots.Count(s => s.CreatorId == this.UserId); - #nullable enable public int GetUsedSlotsForGame(GameVersion version) { return this.database.Slots.Count(s => s.CreatorId == this.UserId && s.GameVersion == version); } - #nullable disable [JsonIgnore] [XmlIgnore] diff --git a/docker-compose.yml b/docker-compose.yml index 2894ce8d..38f78ded 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3' +version: '3.4' volumes: database: redis: @@ -16,6 +16,7 @@ services: test: wget --spider -t1 -nv http://localhost:10061/LITTLEBIGPLANETPS3_XML/status || exit 1 timeout: 10s retries: 5 + start_period: 60s depends_on: db: condition: service_healthy @@ -36,6 +37,7 @@ services: test: wget --spider -t1 -nv http://localhost:10060/status || exit 1 timeout: 10s retries: 5 + start_period: 60s depends_on: db: condition: service_healthy @@ -56,6 +58,7 @@ services: test: wget --spider -t1 -nv http://localhost:10062/api/v1/status || exit 1 timeout: 10s retries: 5 + start_period: 60s depends_on: db: condition: service_healthy @@ -68,8 +71,6 @@ services: image: mariadb container_name: db restart: unless-stopped - ports: - - "3306:3306" environment: MARIADB_ROOT_PASSWORD: lighthouse MARIADB_DATABASE: lighthouse @@ -84,7 +85,5 @@ services: image: redis/redis-stack-server container_name: redis restart: unless-stopped - ports: - - "6379:6379" volumes: - "redis:/var/lib/redis" \ No newline at end of file diff --git a/scripts-and-tools/docker-entrypoint.sh b/scripts-and-tools/docker-entrypoint.sh index 48bab418..13beb189 100644 --- a/scripts-and-tools/docker-entrypoint.sh +++ b/scripts-and-tools/docker-entrypoint.sh @@ -1,15 +1,33 @@ #!/bin/sh -chown -R lighthouse:lighthouse /lighthouse/data +log() { + local type="$1"; shift + printf '%s [%s] [Entrypoint]: %s\n' "$(date -Iseconds)" "$type" "$*" +} -if [ -d "/lighthouse/temp" ]; then - cp -rf /lighthouse/temp/* /lighthouse/data - rm -rf /lighthouse/temp +log Note "Entrypoint script for Lighthouse $SERVER started". + +if [ ! -d "/lighthouse/data" ]; then + log Note "Creating data directory" + mkdir -p "/lighthouse/data" fi -# run from cmd +owner=$(stat -c "%U %G" /lighthouse/data) +if [ owner != "lighthouse lighthouse" ]; then + log Note "Changing ownership of data directory" + chown -R lighthouse:lighthouse /lighthouse/data +fi +if [ -d "/lighthouse/temp" ]; then + log Note "Copying temp directory to data" + cp -rn /lighthouse/temp/* /lighthouse/data + rm -rf /lighthouse/temp +fi + +# Start server + +log Note "Startup tasks finished, starting $SERVER..." cd /lighthouse/data exec su-exec lighthouse:lighthouse dotnet /lighthouse/app/LBPUnion.ProjectLighthouse.Servers."$SERVER".dll -exit $? # Expose error code from dotnet command +exit $? # Expose error code from dotnet command \ No newline at end of file