mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-05-15 14:12:27 +00:00
Make PhotoSubjects use a one to many relationship (#687)
* Attempt to remodel PhotoSubject and Photo relationship
* Fix migration name
* Use exactName for migration lock
* Revert "Use exactName for migration lock"
This reverts commit 76cee6a3ff
.
* Set command timeout to 5 minutes for database migrations
* Delete unused PhotoSubjects in migration
* Clean up website queries and finalize subject refactor
* Add migration to remove PhotoSubjectCollection
* Add grace period for container startup and optimize startup
* Make config backup copy original file
* Allow docker entrypoint to fix data permissions
This commit is contained in:
parent
35ea2682b9
commit
017dcd6888
22 changed files with 240 additions and 210 deletions
|
@ -54,9 +54,7 @@ public class ClientConfigurationController : ControllerBase
|
|||
[Produces("text/xml")]
|
||||
public async Task<IActionResult> 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<IActionResult> 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<PrivacySettings>();
|
||||
|
|
|
@ -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;
|
||||
|
||||
this.database.Photos.Add(photo);
|
||||
|
||||
// Save to get photo ID for the PhotoSubject foreign keys
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
if (photo.XmlSubjects != null)
|
||||
{
|
||||
// Check for duplicate photo subjects
|
||||
List<string> subjectUserIds = new(4);
|
||||
foreach (PhotoSubject subject in photo.Subjects)
|
||||
foreach (PhotoSubject subject in photo.PhotoSubjects)
|
||||
{
|
||||
if (subjectUserIds.Contains(subject.Username) && !string.IsNullOrEmpty(subject.Username)) return this.BadRequest();
|
||||
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)))
|
||||
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();
|
||||
|
||||
photo.PhotoSubjectIds = photo.Subjects.Where(s => s.UserId != 0).Select(subject => subject.PhotoSubjectId.ToString()).ToArray();
|
||||
|
||||
Logger.Debug($"Adding PhotoSubjectCollection ({photo.PhotoSubjectCollection}) to photo", LogArea.Photos);
|
||||
|
||||
this.database.Photos.Add(photo);
|
||||
}
|
||||
|
||||
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<Photo> 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<Photo> photos = await this.database.Photos.Include
|
||||
(p => p.Creator)
|
||||
List<Photo> 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<int> photoSubjectIds = new();
|
||||
photoSubjectIds.AddRange(this.database.PhotoSubjects.Where(p => p.UserId == targetUserId).Select(p => p.PhotoSubjectId));
|
||||
List<Photo> 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<Photo> 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<string> photoResources = new(){photo.LargeHash, photo.SmallHash, photo.MediumHash, photo.PlanHash,};
|
||||
foreach (string hash in photoResources)
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -76,21 +76,21 @@
|
|||
</i>
|
||||
</p>
|
||||
|
||||
@if (Model.Subjects.Count > 0)
|
||||
@if (Model.PhotoSubjects.Count > 0)
|
||||
{
|
||||
<p>
|
||||
<b>Photo contains @Model.Subjects.Count @(Model.Subjects.Count == 1 ? "person" : "people"):</b>
|
||||
<b>Photo contains @Model.PhotoSubjects.Count @(Model.PhotoSubjects.Count == 1 ? "person" : "people"):</b>
|
||||
</p>
|
||||
}
|
||||
<div id="hover-subjects-@Model.PhotoId">
|
||||
@foreach (PhotoSubject subject in Model.Subjects)
|
||||
@foreach (PhotoSubject subject in Model.PhotoSubjects)
|
||||
{
|
||||
@await subject.User.ToLink(Html, ViewData, language, timeZone)
|
||||
}
|
||||
</div>
|
||||
|
||||
@{
|
||||
PhotoSubject[] subjects = Model.Subjects.ToArray();
|
||||
PhotoSubject[] subjects = Model.PhotoSubjects.ToArray();
|
||||
foreach (PhotoSubject subject in subjects) subject.Username = subject.User.Username;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Photo> 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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -57,26 +57,22 @@ public class SlotsPage : BaseLayout
|
|||
|
||||
string trimmedSearch = finalSearch.ToString().Trim();
|
||||
|
||||
this.SlotCount = await this.Database.Slots.Include(p => p.Creator)
|
||||
IQueryable<Slot> 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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<int> subjectUserIds = new(4);
|
||||
foreach (PhotoSubject subject in photo.Subjects)
|
||||
foreach (PhotoSubject subject in photo.PhotoSubjects)
|
||||
{
|
||||
if (subjectUserIds.Contains(subject.UserId))
|
||||
{
|
||||
|
|
|
@ -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<string> subjectCollections = new();
|
||||
List<int> 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<PhotoSubject> 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -117,7 +117,7 @@ public abstract class ConfigurationBase<T> 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);
|
||||
|
|
|
@ -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<User?> 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<User?> 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<GameToken?> GameTokenFromRequest(HttpRequest request)
|
||||
|
|
|
@ -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<int>(
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<string>(
|
||||
name: "PhotoSubjectCollection",
|
||||
table: "Photos",
|
||||
type: "longtext",
|
||||
nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
// <auto-generated />
|
||||
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<string>("PhotoSubjectCollection")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("PlanHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
@ -719,11 +714,16 @@ namespace ProjectLighthouse.Migrations
|
|||
b.Property<string>("Bounds")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("PhotoId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<PhotoSubject>? _subjects;
|
||||
|
||||
[NotMapped]
|
||||
[XmlElement("slot")]
|
||||
public PhotoSlot? XmlLevelInfo;
|
||||
|
@ -44,7 +39,7 @@ public class Photo
|
|||
[NotMapped]
|
||||
[XmlArray("subjects")]
|
||||
[XmlArrayItem("subject")]
|
||||
public List<PhotoSubject>? SubjectsXmlDontUseLiterallyEver;
|
||||
public List<PhotoSubject>? XmlSubjects;
|
||||
|
||||
[Key]
|
||||
public int PhotoId { get; set; }
|
||||
|
@ -65,40 +60,7 @@ public class Photo
|
|||
[XmlElement("plan")]
|
||||
public string PlanHash { get; set; } = "";
|
||||
|
||||
[NotMapped]
|
||||
public List<PhotoSubject> Subjects {
|
||||
get {
|
||||
if (this.SubjectsXmlDontUseLiterallyEver != null) return this.SubjectsXmlDontUseLiterallyEver;
|
||||
if (this._subjects != null) return this._subjects;
|
||||
|
||||
List<PhotoSubject> 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<PhotoSubject> PhotoSubjects { get; set; } = new HashSet<PhotoSubject>();
|
||||
|
||||
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) +
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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<int> 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));
|
||||
|
||||
/// <summary>
|
||||
/// The location of the profile card on the user's earth
|
||||
|
@ -243,7 +235,7 @@ public class User
|
|||
LbpSerializer.StringElement<int>("reviewCount", this.Reviews, true) +
|
||||
LbpSerializer.StringElement<int>("commentCount", this.Comments, true) +
|
||||
LbpSerializer.StringElement<int>("photosByMeCount", this.PhotosByMe, true) +
|
||||
LbpSerializer.StringElement<int>("photosWithMeCount", this.PhotosWithMe(), true) +
|
||||
LbpSerializer.StringElement<int>("photosWithMeCount", this.PhotosWithMe, true) +
|
||||
LbpSerializer.StringElement("commentsEnabled", ServerConfiguration.Instance.UserGeneratedContentLimits.ProfileCommentsEnabled && this.CommentsEnabled) +
|
||||
LbpSerializer.StringElement("location", this.Location.Serialize()) +
|
||||
LbpSerializer.StringElement<int>("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]
|
||||
|
|
|
@ -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"
|
|
@ -1,14 +1,32 @@
|
|||
#!/bin/sh
|
||||
|
||||
chown -R lighthouse:lighthouse /lighthouse/data
|
||||
log() {
|
||||
local type="$1"; shift
|
||||
printf '%s [%s] [Entrypoint]: %s\n' "$(date -Iseconds)" "$type" "$*"
|
||||
}
|
||||
|
||||
log Note "Entrypoint script for Lighthouse $SERVER started".
|
||||
|
||||
if [ ! -d "/lighthouse/data" ]; then
|
||||
log Note "Creating data directory"
|
||||
mkdir -p "/lighthouse/data"
|
||||
fi
|
||||
|
||||
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
|
||||
cp -rf /lighthouse/temp/* /lighthouse/data
|
||||
log Note "Copying temp directory to data"
|
||||
cp -rn /lighthouse/temp/* /lighthouse/data
|
||||
rm -rf /lighthouse/temp
|
||||
fi
|
||||
|
||||
# run from cmd
|
||||
# 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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue