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:
Josh 2023-02-23 22:20:55 -06:00 committed by GitHub
parent 35ea2682b9
commit 017dcd6888
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 240 additions and 210 deletions

View file

@ -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>();

View file

@ -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)

View file

@ -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();

View file

@ -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;
}

View file

@ -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();

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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))
{

View file

@ -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();
}
}

View file

@ -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);

View file

@ -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)

View file

@ -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");
}
}
}

View file

@ -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");
}
}
}

View file

@ -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
}
}

View file

@ -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);
}
}

View file

@ -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) +

View file

@ -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; }

View file

@ -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]

View file

@ -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"

View file

@ -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