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