Add MCDO (#31)

* rudimentary charadata first impl

* update submodule

* update properly

* some fixes etc

* add cascade and some other fixes

* most of charadata impl

* dotnet 9

* net9 and migration to k4os

* bum papi

---------

Co-authored-by: Stanley Dimant <root.darkarchon@outlook.com>
This commit is contained in:
rootdarkarchon
2025-01-11 22:42:55 +01:00
committed by Loporrit
parent 084c53ee20
commit 3fbceee0ce
22 changed files with 6844 additions and 45 deletions

View File

@@ -1,5 +1,4 @@
using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.SignalR;
using MareSynchronos.API.SignalR;
using MareSynchronosServer.Hubs;
using MareSynchronosShared.Utils;
using Microsoft.AspNetCore.Authorization;

View File

@@ -0,0 +1,555 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronosServer.Utils;
using MareSynchronosShared.Models;
using MareSynchronosShared.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using System.Text;
using System.Text.Json;
namespace MareSynchronosServer.Hubs;
public partial class MareHub
{
[Authorize(Policy = "Identified")]
public async Task<CharaDataFullDto?> CharaDataCreate()
{
_logger.LogCallInfo();
int uploadCount = DbContext.CharaData.Count(c => c.UploaderUID == UserUID);
User user = DbContext.Users.Single(u => u.UID == UserUID);
int maximumUploads = _maxCharaDataByUser;
if (uploadCount >= maximumUploads)
{
return null;
}
string charaDataId = null;
while (charaDataId == null)
{
charaDataId = StringUtils.GenerateRandomString(10, "abcdefghijklmnopqrstuvwxyzABCDEFHIJKLMNOPQRSTUVWXYZ");
bool idExists = await DbContext.CharaData.AnyAsync(c => c.UploaderUID == UserUID && c.Id == charaDataId).ConfigureAwait(false);
if (idExists)
{
charaDataId = null;
}
}
DateTime createdDate = DateTime.UtcNow;
CharaData charaData = new()
{
Id = charaDataId,
UploaderUID = UserUID,
CreatedDate = createdDate,
UpdatedDate = createdDate,
AccessType = CharaDataAccess.Individuals,
ShareType = CharaDataShare.Private,
CustomizeData = string.Empty,
GlamourerData = string.Empty,
ExpiryDate = DateTime.MaxValue,
Description = string.Empty,
};
await DbContext.CharaData.AddAsync(charaData).ConfigureAwait(false);
await DbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", charaDataId));
return GetCharaDataFullDto(charaData);
}
[Authorize(Policy = "Identified")]
public async Task<bool> CharaDataDelete(string id)
{
var existingData = await DbContext.CharaData.SingleOrDefaultAsync(u => u.Id == id && u.UploaderUID == UserUID).ConfigureAwait(false);
if (existingData == null)
return false;
try
{
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", id));
DbContext.Remove(existingData);
await DbContext.SaveChangesAsync().ConfigureAwait(false);
return true;
}
catch (Exception ex)
{
_logger.LogCallWarning(MareHubLogger.Args("FAILURE", id, ex.Message));
return false;
}
}
[Authorize(Policy = "Identified")]
public async Task<CharaDataDownloadDto?> CharaDataDownload(string id)
{
CharaData charaData = await GetCharaDataById(id, nameof(CharaDataDownload)).ConfigureAwait(false);
if (!string.Equals(charaData.UploaderUID, UserUID, StringComparison.Ordinal))
{
charaData.DownloadCount++;
await DbContext.SaveChangesAsync().ConfigureAwait(false);
}
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", id));
return GetCharaDataDownloadDto(charaData);
}
[Authorize(Policy = "Identified")]
public async Task<CharaDataMetaInfoDto?> CharaDataGetMetainfo(string id)
{
var charaData = await GetCharaDataById(id, nameof(CharaDataGetMetainfo)).ConfigureAwait(false);
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", id));
return GetCharaDataMetaInfoDto(charaData);
}
[Authorize(Policy = "Identified")]
public async Task<List<CharaDataFullDto>> CharaDataGetOwn()
{
var ownCharaData = await DbContext.CharaData
.Include(u => u.Files)
.Include(u => u.FileSwaps)
.Include(u => u.OriginalFiles)
.Include(u => u.AllowedIndividiuals)
.ThenInclude(u => u.AllowedUser)
.Include(u => u.Poses)
.AsSplitQuery()
.Where(c => c.UploaderUID == UserUID).ToListAsync().ConfigureAwait(false);
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS"));
return [.. ownCharaData.Select(GetCharaDataFullDto)];
}
[Authorize(Policy = "Identified")]
public async Task<List<CharaDataMetaInfoDto>> CharaDataGetShared()
{
_logger.LogCallInfo();
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
List<CharaData> sharedCharaData = [];
foreach (var pair in allPairedUsers)
{
var allSharedDataByPair = await DbContext.CharaData
.Include(u => u.Files)
.Include(u => u.OriginalFiles)
.Include(u => u.AllowedIndividiuals)
.Include(u => u.Poses)
.Include(u => u.Uploader)
.Where(p => p.ShareType == CharaDataShare.Shared && p.UploaderUID == pair)
.AsSplitQuery()
.AsNoTracking()
.ToListAsync()
.ConfigureAwait(false);
foreach (var charaData in allSharedDataByPair)
{
if (await CheckCharaDataAllowance(charaData).ConfigureAwait(false))
{
sharedCharaData.Add(charaData);
}
}
}
var charaDataDirectlyShared = await DbContext.CharaData.Include(u => u.Files)
.Include(u => u.OriginalFiles)
.Include(u => u.AllowedIndividiuals)
.Include(u => u.Poses)
.Include(u => u.Uploader)
.Where(p => p.ShareType == CharaDataShare.Shared && p.AllowedIndividiuals.Any(u => u.AllowedUserUID == UserUID))
.AsSplitQuery()
.AsNoTracking()
.ToListAsync()
.ConfigureAwait(false);
foreach (var data in charaDataDirectlyShared)
{
if (sharedCharaData.Exists(d => string.Equals(d.Id, data.Id, StringComparison.Ordinal)
&& string.Equals(d.UploaderUID, d.UploaderUID, StringComparison.Ordinal)))
continue;
sharedCharaData.Add(data);
}
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", sharedCharaData.Count));
return [.. sharedCharaData.Select(GetCharaDataMetaInfoDto)];
}
[Authorize(Policy = "Identified")]
public async Task<CharaDataFullDto?> CharaDataUpdate(CharaDataUpdateDto updateDto)
{
var charaData = await DbContext.CharaData
.Include(u => u.Files)
.Include(u => u.OriginalFiles)
.Include(u => u.AllowedIndividiuals)
.ThenInclude(u => u.AllowedUser)
.Include(u => u.FileSwaps)
.Include(u => u.Poses)
.AsSplitQuery()
.SingleOrDefaultAsync(u => u.Id == updateDto.Id && u.UploaderUID == UserUID).ConfigureAwait(false);
if (charaData == null)
return null;
bool anyChanges = false;
if (updateDto.Description != null)
{
charaData.Description = updateDto.Description;
anyChanges = true;
}
if (updateDto.ExpiryDate != null)
{
charaData.ExpiryDate = updateDto.ExpiryDate;
anyChanges = true;
}
if (updateDto.GlamourerData != null)
{
charaData.GlamourerData = updateDto.GlamourerData;
anyChanges = true;
}
if (updateDto.CustomizeData != null)
{
charaData.CustomizeData = updateDto.CustomizeData;
anyChanges = true;
}
if (updateDto.ManipulationData != null)
{
charaData.ManipulationData = updateDto.ManipulationData;
anyChanges = true;
}
if (updateDto.AccessType != null)
{
charaData.AccessType = GetAccessType(updateDto.AccessType.Value);
anyChanges = true;
}
if (updateDto.ShareType != null)
{
charaData.ShareType = GetShareType(updateDto.ShareType.Value);
anyChanges = true;
}
if (updateDto.AllowedUsers != null)
{
var individuals = charaData.AllowedIndividiuals.ToList();
charaData.AllowedIndividiuals.Clear();
DbContext.RemoveRange(individuals);
var allowedUserList = updateDto.AllowedUsers.ToList();
foreach (var user in updateDto.AllowedUsers)
{
var dbUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == user || u.Alias == user).ConfigureAwait(false);
if (dbUser != null)
{
if (!allowedUserList.Contains(dbUser.UID, StringComparer.Ordinal) && !allowedUserList.Contains(dbUser.Alias, StringComparer.Ordinal))
{
continue;
}
allowedUserList.RemoveAll(u => string.Equals(u, dbUser.UID, StringComparison.Ordinal));
allowedUserList.RemoveAll(u => string.Equals(u, dbUser.Alias, StringComparison.Ordinal));
charaData.AllowedIndividiuals.Add(new CharaDataAllowance()
{
AllowedUser = dbUser,
Parent = charaData
});
}
}
anyChanges = true;
}
if (updateDto.FileGamePaths != null)
{
var originalFiles = charaData.OriginalFiles.ToList();
charaData.OriginalFiles.Clear();
DbContext.RemoveRange(originalFiles);
var files = charaData.Files.ToList();
charaData.Files.Clear();
DbContext.RemoveRange(files);
foreach (var file in updateDto.FileGamePaths)
{
charaData.Files.Add(new CharaDataFile()
{
FileCacheHash = file.HashOrFileSwap,
GamePath = file.GamePath,
Parent = charaData
});
charaData.OriginalFiles.Add(new CharaDataOriginalFile()
{
Hash = file.HashOrFileSwap,
Parent = charaData,
GamePath = file.GamePath
});
}
anyChanges = true;
}
if (updateDto.FileSwaps != null)
{
var fileSwaps = charaData.FileSwaps.ToList();
charaData.FileSwaps.Clear();
DbContext.RemoveRange(fileSwaps);
foreach (var file in updateDto.FileSwaps)
{
charaData.FileSwaps.Add(new CharaDataFileSwap()
{
FilePath = file.HashOrFileSwap,
GamePath = file.GamePath,
Parent = charaData
});
}
anyChanges = true;
}
if (updateDto.Poses != null)
{
foreach (var pose in updateDto.Poses)
{
if (pose.Id == null)
{
charaData.Poses.Add(new CharaDataPose()
{
Description = pose.Description,
Parent = charaData,
ParentUploaderUID = UserUID,
PoseData = pose.PoseData,
WorldData = pose.WorldData == null ? string.Empty : JsonSerializer.Serialize(pose.WorldData),
});
anyChanges = true;
}
else
{
var associatedPose = charaData.Poses.FirstOrDefault(p => p.Id == pose.Id);
if (associatedPose == null)
continue;
if (pose.Description == null && pose.PoseData == null && pose.WorldData == null)
{
charaData.Poses.Remove(associatedPose);
DbContext.Remove(associatedPose);
}
else
{
if (pose.Description != null)
associatedPose.Description = pose.Description;
if (pose.WorldData != null)
{
if (pose.WorldData.Value == default) associatedPose.WorldData = string.Empty;
else associatedPose.WorldData = JsonSerializer.Serialize(pose.WorldData.Value);
}
if (pose.PoseData != null)
associatedPose.PoseData = pose.PoseData;
}
anyChanges = true;
}
var overflowingPoses = charaData.Poses.Skip(10).ToList();
foreach (var overflowing in overflowingPoses)
{
charaData.Poses.Remove(overflowing);
DbContext.Remove(overflowing);
}
}
}
if (anyChanges)
{
charaData.UpdatedDate = DateTime.UtcNow;
await DbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", anyChanges));
}
return GetCharaDataFullDto(charaData);
}
private static CharaDataAccess GetAccessType(AccessTypeDto dataAccess) => dataAccess switch
{
AccessTypeDto.Public => CharaDataAccess.Public,
AccessTypeDto.AllPairs => CharaDataAccess.AllPairs,
AccessTypeDto.ClosePairs => CharaDataAccess.ClosePairs,
AccessTypeDto.Individuals => CharaDataAccess.Individuals,
_ => throw new NotSupportedException(),
};
private static AccessTypeDto GetAccessTypeDto(CharaDataAccess dataAccess) => dataAccess switch
{
CharaDataAccess.Public => AccessTypeDto.Public,
CharaDataAccess.AllPairs => AccessTypeDto.AllPairs,
CharaDataAccess.ClosePairs => AccessTypeDto.ClosePairs,
CharaDataAccess.Individuals => AccessTypeDto.Individuals,
_ => throw new NotSupportedException(),
};
private static CharaDataDownloadDto GetCharaDataDownloadDto(CharaData charaData)
{
return new CharaDataDownloadDto(charaData.Id, charaData.Uploader.ToUserData())
{
CustomizeData = charaData.CustomizeData,
Description = charaData.Description,
FileGamePaths = charaData.Files.Select(k => new GamePathEntry(k.FileCacheHash, k.GamePath)).ToList(),
GlamourerData = charaData.GlamourerData,
FileSwaps = charaData.FileSwaps.Select(k => new GamePathEntry(k.FilePath, k.GamePath)).ToList(),
ManipulationData = charaData.ManipulationData,
};
}
private CharaDataFullDto GetCharaDataFullDto(CharaData charaData)
{
return new CharaDataFullDto(charaData.Id, new(UserUID))
{
AccessType = GetAccessTypeDto(charaData.AccessType),
ShareType = GetShareTypeDto(charaData.ShareType),
AllowedUsers = [.. charaData.AllowedIndividiuals.Select(u => new UserData(u.AllowedUser.UID, u.AllowedUser.Alias))],
CustomizeData = charaData.CustomizeData,
Description = charaData.Description,
ExpiryDate = charaData.ExpiryDate ?? DateTime.MaxValue,
OriginalFiles = charaData.OriginalFiles.Select(k => new GamePathEntry(k.Hash, k.GamePath)).ToList(),
FileGamePaths = charaData.Files.Select(k => new GamePathEntry(k.FileCacheHash, k.GamePath)).ToList(),
FileSwaps = charaData.FileSwaps.Select(k => new GamePathEntry(k.FilePath, k.GamePath)).ToList(),
GlamourerData = charaData.GlamourerData,
CreatedDate = charaData.CreatedDate,
UpdatedDate = charaData.UpdatedDate,
ManipulationData = charaData.ManipulationData,
DownloadCount = charaData.DownloadCount,
PoseData = [.. charaData.Poses.OrderBy(p => p.Id).Select(k =>
{
WorldData data = default;
if(!string.IsNullOrEmpty(k.WorldData)) data = JsonSerializer.Deserialize<WorldData>(k.WorldData);
return new PoseEntry(k.Id)
{
Description = k.Description,
PoseData = k.PoseData,
WorldData = data
};
})],
};
}
private static CharaDataMetaInfoDto GetCharaDataMetaInfoDto(CharaData charaData)
{
var allOrigHashes = charaData.OriginalFiles.Select(k => k.Hash).ToList();
var allFileHashes = charaData.Files.Select(f => f.FileCacheHash).ToList();
var allHashesPresent = allOrigHashes.TrueForAll(h => allFileHashes.Contains(h, StringComparer.Ordinal));
var canBeDownloaded = allHashesPresent &= !string.IsNullOrEmpty(charaData.GlamourerData);
return new CharaDataMetaInfoDto(charaData.Id, charaData.Uploader.ToUserData())
{
CanBeDownloaded = canBeDownloaded,
Description = charaData.Description,
UpdatedDate = charaData.UpdatedDate,
PoseData = [.. charaData.Poses.OrderBy(p => p.Id).Select(k =>
{
WorldData data = default;
if(!string.IsNullOrEmpty(k.WorldData)) data = JsonSerializer.Deserialize<WorldData>(k.WorldData);
return new PoseEntry(k.Id)
{
Description = k.Description,
PoseData = k.PoseData,
WorldData = data
};
})],
};
}
private static CharaDataShare GetShareType(ShareTypeDto dataShare) => dataShare switch
{
ShareTypeDto.Shared => CharaDataShare.Shared,
ShareTypeDto.Private => CharaDataShare.Private,
_ => throw new NotSupportedException(),
};
private static ShareTypeDto GetShareTypeDto(CharaDataShare dataShare) => dataShare switch
{
CharaDataShare.Shared => ShareTypeDto.Shared,
CharaDataShare.Private => ShareTypeDto.Private,
_ => throw new NotSupportedException(),
};
private async Task<bool> CheckCharaDataAllowance(CharaData charaData)
{
// check for self
if (string.Equals(charaData.UploaderUID, UserUID, StringComparison.Ordinal))
return true;
// check for public access
if (charaData.AccessType == CharaDataAccess.Public)
return true;
// check for individuals
if (charaData.AllowedIndividiuals.Any(u => string.Equals(u.AllowedUserUID, UserUID, StringComparison.Ordinal)))
return true;
var pairInfoUploader = await GetAllPairedUnpausedUsers(charaData.UploaderUID).ConfigureAwait(false);
// check for all pairs
if (charaData.AccessType == CharaDataAccess.AllPairs)
{
if (pairInfoUploader.Any(pair => string.Equals(pair, UserUID, StringComparison.Ordinal)))
return true;
return false;
}
// check for individual pairs
if (charaData.AccessType == CharaDataAccess.ClosePairs)
{
if (pairInfoUploader.Any(pair => string.Equals(pair, UserUID, StringComparison.Ordinal)))
{
ClientPair callerPair =
await DbContext.ClientPairs.AsNoTracking().SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == charaData.UploaderUID).ConfigureAwait(false);
ClientPair uploaderPair =
await DbContext.ClientPairs.AsNoTracking().SingleOrDefaultAsync(w => w.UserUID == charaData.UploaderUID && w.OtherUserUID == UserUID).ConfigureAwait(false);
return (callerPair != null && uploaderPair != null);
}
return false;
}
return false;
}
private async Task<CharaData> GetCharaDataById(string id, string methodName)
{
var splitid = id.Split(":", StringSplitOptions.None);
if (splitid.Length != 2)
{
_logger.LogCallWarning(MareHubLogger.Args("INVALID", id));
throw new InvalidOperationException($"Id {id} not in expected format");
}
var charaData = await DbContext.CharaData
.Include(u => u.Files)
.Include(u => u.FileSwaps)
.Include(u => u.AllowedIndividiuals)
.Include(u => u.Poses)
.Include(u => u.Uploader)
.AsSplitQuery()
.SingleOrDefaultAsync(c => c.Id == splitid[1] && c.UploaderUID == splitid[0]).ConfigureAwait(false);
if (charaData == null)
{
_logger.LogCallWarning(MareHubLogger.Args("NOT FOUND", id));
throw new InvalidDataException($"No chara data with {id} found");
}
if (!await CheckCharaDataAllowance(charaData).ConfigureAwait(false))
{
_logger.LogCallWarning(MareHubLogger.Args("UNAUTHORIZED", id));
throw new UnauthorizedAccessException($"User is not allowed to download {id}");
}
return charaData;
}
}

View File

@@ -2,7 +2,6 @@
using Microsoft.EntityFrameworkCore;
using MareSynchronosServer.Utils;
using MareSynchronosShared.Utils;
using Microsoft.IdentityModel.Tokens;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Group;
using MareSynchronosShared.Metrics;
@@ -113,7 +112,7 @@ public partial class MareHub
private async Task<string> GetUserIdent(string uid)
{
if (uid.IsNullOrEmpty()) return string.Empty;
if (string.IsNullOrEmpty(uid)) return string.Empty;
return await _redis.GetAsync<string>("UID:" + uid).ConfigureAwait(false);
}

View File

@@ -48,6 +48,7 @@ public partial class MareHub : Hub<IMareHub>, IMareHub
_maxGroupUserCount = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 100);
_fileServerAddress = configuration.GetValue<Uri>(nameof(ServerConfiguration.CdnFullUrl));
_expectedClientVersion = configuration.GetValueOrDefault(nameof(ServerConfiguration.ExpectedClientVersion), new Version(0, 0, 0));
_maxCharaDataByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxCharaDataByUser), 10);
_contextAccessor = contextAccessor;
_redis = redisDb;
_logger = new MareHubLogger(this, logger);
@@ -91,7 +92,8 @@ public partial class MareHub : Hub<IMareHub>, IMareHub
ShardName = _shardName,
MaxGroupsJoinedByUser = _maxJoinedGroupsByUser,
MaxGroupUserCount = _maxGroupUserCount,
FileServerAddress = _fileServerAddress
FileServerAddress = _fileServerAddress,
MaxCharaData = _maxCharaDataByUser
},
};
}

View File

@@ -0,0 +1,50 @@
using MareSynchronosShared.Data;
using Microsoft.EntityFrameworkCore;
namespace MareSynchronosServer.Services;
public class CharaDataCleanupService : IHostedService
{
private readonly ILogger<CharaDataCleanupService> _logger;
private readonly IDbContextFactory<MareDbContext> _dbContextFactory;
private readonly CancellationTokenSource _cleanupCts = new();
public CharaDataCleanupService(ILogger<CharaDataCleanupService> logger, IDbContextFactory<MareDbContext> dbContextFactory)
{
_logger = logger;
_dbContextFactory = dbContextFactory;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_ = Cleanup(cancellationToken);
return Task.CompletedTask;
}
private async Task Cleanup(CancellationToken ct)
{
_logger.LogInformation("CharaData Cleanup Service started");
while (!ct.IsCancellationRequested)
{
using (var db = await _dbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false))
{
var dateTime = DateTime.UtcNow;
var expiredData = await db.CharaData.Where(c => c.ExpiryDate <= DateTime.UtcNow).ToListAsync(cancellationToken: ct).ConfigureAwait(false);
_logger.LogInformation("Removing {count} expired Chara Data entries", expiredData.Count);
db.RemoveRange(expiredData);
await db.SaveChangesAsync(ct).ConfigureAwait(false);
}
await Task.Delay(TimeSpan.FromHours(12), ct).ConfigureAwait(false);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_cleanupCts?.Cancel();
_cleanupCts?.Dispose();
return Task.CompletedTask;
}
}

View File

@@ -12,15 +12,15 @@ public class UserCleanupService : IHostedService
{
private readonly MareMetrics metrics;
private readonly ILogger<UserCleanupService> _logger;
private readonly IServiceProvider _services;
private readonly IDbContextFactory<MareDbContext> _mareDbContextFactory;
private readonly IConfigurationService<ServerConfiguration> _configuration;
private CancellationTokenSource _cleanupCts;
public UserCleanupService(MareMetrics metrics, ILogger<UserCleanupService> logger, IServiceProvider services, IConfigurationService<ServerConfiguration> configuration)
public UserCleanupService(MareMetrics metrics, ILogger<UserCleanupService> logger, IDbContextFactory<MareDbContext> mareDbContextFactory, IConfigurationService<ServerConfiguration> configuration)
{
this.metrics = metrics;
_logger = logger;
_services = services;
_mareDbContextFactory = mareDbContextFactory;
_configuration = configuration;
}
@@ -38,16 +38,17 @@ public class UserCleanupService : IHostedService
{
while (!ct.IsCancellationRequested)
{
using var scope = _services.CreateScope();
using var dbContext = scope.ServiceProvider.GetService<MareDbContext>()!;
using (var dbContext = await _mareDbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false))
{
CleanUpOutdatedLodestoneAuths(dbContext);
CleanUpOutdatedLodestoneAuths(dbContext);
await PurgeUnusedAccounts(dbContext).ConfigureAwait(false);
await PurgeUnusedAccounts(dbContext).ConfigureAwait(false);
await PurgeTempInvites(dbContext).ConfigureAwait(false);
await PurgeTempInvites(dbContext).ConfigureAwait(false);
dbContext.SaveChanges();
dbContext.SaveChanges();
}
var now = DateTime.Now;
TimeOnly currentTime = new(now.Hour, now.Minute, now.Second);

View File

@@ -97,6 +97,8 @@ public class Startup
{
services.AddSingleton<UserCleanupService>();
services.AddHostedService(provider => provider.GetService<UserCleanupService>());
services.AddSingleton<CharaDataCleanupService>();
services.AddHostedService(provider => provider.GetService<CharaDataCleanupService>());
}
}

View File

@@ -45,37 +45,84 @@ public class MareDbContext : DbContext
public DbSet<UserProfileData> UserProfileData { get; set; }
public DbSet<UserProfileDataReport> UserProfileReports { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<CharaData> CharaData { get; set; }
public DbSet<CharaDataFile> CharaDataFiles { get; set; }
public DbSet<CharaDataFileSwap> CharaDataFileSwaps { get; set; }
public DbSet<CharaDataOriginalFile> CharaDataOriginalFiles { get; set; }
public DbSet<CharaDataPose> CharaDataPoses { get; set; }
public DbSet<CharaDataAllowance> CharaDataAllowances { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
protected override void OnModelCreating(ModelBuilder mb)
{
modelBuilder.Entity<Auth>().ToTable("auth");
modelBuilder.Entity<User>().ToTable("users");
modelBuilder.Entity<FileCache>().ToTable("file_caches");
modelBuilder.Entity<FileCache>().HasIndex(c => c.UploaderUID);
modelBuilder.Entity<ClientPair>().ToTable("client_pairs");
modelBuilder.Entity<ClientPair>().HasKey(u => new { u.UserUID, u.OtherUserUID });
modelBuilder.Entity<ClientPair>().HasIndex(c => c.UserUID);
modelBuilder.Entity<ClientPair>().HasIndex(c => c.OtherUserUID);
modelBuilder.Entity<ForbiddenUploadEntry>().ToTable("forbidden_upload_entries");
modelBuilder.Entity<Banned>().ToTable("banned_users");
modelBuilder.Entity<LodeStoneAuth>().ToTable("lodestone_auth");
modelBuilder.Entity<BannedRegistrations>().ToTable("banned_registrations");
modelBuilder.Entity<Group>().ToTable("groups");
modelBuilder.Entity<Group>().HasIndex(c => c.OwnerUID);
modelBuilder.Entity<GroupPair>().ToTable("group_pairs");
modelBuilder.Entity<GroupPair>().HasKey(u => new { u.GroupGID, u.GroupUserUID });
modelBuilder.Entity<GroupPair>().HasIndex(c => c.GroupUserUID);
modelBuilder.Entity<GroupPair>().HasIndex(c => c.GroupGID);
modelBuilder.Entity<GroupBan>().ToTable("group_bans");
modelBuilder.Entity<GroupBan>().HasKey(u => new { u.GroupGID, u.BannedUserUID });
modelBuilder.Entity<GroupBan>().HasIndex(c => c.BannedUserUID);
modelBuilder.Entity<GroupBan>().HasIndex(c => c.GroupGID);
modelBuilder.Entity<GroupTempInvite>().ToTable("group_temp_invites");
modelBuilder.Entity<GroupTempInvite>().HasKey(u => new { u.GroupGID, u.Invite });
modelBuilder.Entity<GroupTempInvite>().HasIndex(c => c.GroupGID);
modelBuilder.Entity<GroupTempInvite>().HasIndex(c => c.Invite);
modelBuilder.Entity<UserProfileData>().ToTable("user_profile_data");
modelBuilder.Entity<UserProfileData>().HasKey(c => c.UserUID);
modelBuilder.Entity<UserProfileDataReport>().ToTable("user_profile_data_reports");
mb.Entity<Auth>().ToTable("auth");
mb.Entity<User>().ToTable("users");
mb.Entity<FileCache>().ToTable("file_caches");
mb.Entity<FileCache>().HasIndex(c => c.UploaderUID);
mb.Entity<ClientPair>().ToTable("client_pairs");
mb.Entity<ClientPair>().HasKey(u => new { u.UserUID, u.OtherUserUID });
mb.Entity<ClientPair>().HasIndex(c => c.UserUID);
mb.Entity<ClientPair>().HasIndex(c => c.OtherUserUID);
mb.Entity<ForbiddenUploadEntry>().ToTable("forbidden_upload_entries");
mb.Entity<Banned>().ToTable("banned_users");
mb.Entity<LodeStoneAuth>().ToTable("lodestone_auth");
mb.Entity<BannedRegistrations>().ToTable("banned_registrations");
mb.Entity<Group>().ToTable("groups");
mb.Entity<Group>().HasIndex(c => c.OwnerUID);
mb.Entity<GroupPair>().ToTable("group_pairs");
mb.Entity<GroupPair>().HasKey(u => new { u.GroupGID, u.GroupUserUID });
mb.Entity<GroupPair>().HasIndex(c => c.GroupUserUID);
mb.Entity<GroupPair>().HasIndex(c => c.GroupGID);
mb.Entity<GroupBan>().ToTable("group_bans");
mb.Entity<GroupBan>().HasKey(u => new { u.GroupGID, u.BannedUserUID });
mb.Entity<GroupBan>().HasIndex(c => c.BannedUserUID);
mb.Entity<GroupBan>().HasIndex(c => c.GroupGID);
mb.Entity<GroupTempInvite>().ToTable("group_temp_invites");
mb.Entity<GroupTempInvite>().HasKey(u => new { u.GroupGID, u.Invite });
mb.Entity<GroupTempInvite>().HasIndex(c => c.GroupGID);
mb.Entity<GroupTempInvite>().HasIndex(c => c.Invite);
mb.Entity<UserProfileData>().ToTable("user_profile_data");
mb.Entity<UserProfileData>().HasKey(c => c.UserUID);
mb.Entity<UserProfileDataReport>().ToTable("user_profile_data_reports");
mb.Entity<CharaData>().ToTable("chara_data");
mb.Entity<CharaData>()
.HasMany(p => p.Poses)
.WithOne(c => c.Parent)
.HasForeignKey(c => new { c.ParentId, c.ParentUploaderUID });
mb.Entity<CharaData>()
.HasMany(p => p.Files)
.WithOne(c => c.Parent)
.HasForeignKey(c => new { c.ParentId, c.ParentUploaderUID });
mb.Entity<CharaData>()
.HasMany(p => p.OriginalFiles)
.WithOne(p => p.Parent)
.HasForeignKey(p => new { p.ParentId, p.ParentUploaderUID });
mb.Entity<CharaData>()
.HasMany(p => p.AllowedIndividiuals)
.WithOne(p => p.Parent)
.HasForeignKey(p => new { p.ParentId, p.ParentUploaderUID });
mb.Entity<CharaData>()
.HasMany(p => p.FileSwaps)
.WithOne(p => p.Parent)
.HasForeignKey(p => new { p.ParentId, p.ParentUploaderUID });
mb.Entity<CharaData>().HasKey(p => new { p.Id, p.UploaderUID });
mb.Entity<CharaData>().HasIndex(p => p.UploaderUID);
mb.Entity<CharaData>().HasIndex(p => p.Id);
mb.Entity<CharaDataFile>().ToTable("chara_data_files");
mb.Entity<CharaDataFile>().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.GamePath });
mb.Entity<CharaDataFile>().HasIndex(c => c.ParentId);
mb.Entity<CharaDataFile>().HasOne(f => f.FileCache).WithMany().HasForeignKey(f => f.FileCacheHash).OnDelete(DeleteBehavior.Cascade);
mb.Entity<CharaDataFileSwap>().ToTable("chara_data_file_swaps");
mb.Entity<CharaDataFileSwap>().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.GamePath });
mb.Entity<CharaDataFileSwap>().HasIndex(c => c.ParentId);
mb.Entity<CharaDataPose>().ToTable("chara_data_poses");
mb.Entity<CharaDataPose>().Property(p => p.Id).ValueGeneratedOnAdd();
mb.Entity<CharaDataPose>().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.Id });
mb.Entity<CharaDataPose>().HasIndex(c => c.ParentId);
mb.Entity<CharaDataOriginalFile>().ToTable("chara_data_orig_files");
mb.Entity<CharaDataOriginalFile>().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.GamePath });
mb.Entity<CharaDataOriginalFile>().HasIndex(c => c.ParentId);
mb.Entity<CharaDataAllowance>().ToTable("chara_data_allowance");
mb.Entity<CharaDataAllowance>().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.AllowedUserUID });
mb.Entity<CharaDataAllowance>().HasIndex(c => c.ParentId);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,198 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MareSynchronosServer.Migrations
{
/// <inheritdoc />
public partial class CharaData : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "chara_data",
columns: table => new
{
id = table.Column<string>(type: "text", nullable: false),
uploader_uid = table.Column<string>(type: "character varying(10)", nullable: false),
created_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
updated_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
description = table.Column<string>(type: "text", nullable: true),
access_type = table.Column<int>(type: "integer", nullable: false),
share_type = table.Column<int>(type: "integer", nullable: false),
expiry_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
glamourer_data = table.Column<string>(type: "text", nullable: true),
customize_data = table.Column<string>(type: "text", nullable: true),
download_count = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_chara_data", x => new { x.id, x.uploader_uid });
table.ForeignKey(
name: "fk_chara_data_users_uploader_uid",
column: x => x.uploader_uid,
principalTable: "users",
principalColumn: "uid",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "chara_data_allowance",
columns: table => new
{
parent_id = table.Column<string>(type: "text", nullable: false),
parent_uploader_uid = table.Column<string>(type: "character varying(10)", nullable: false),
allowed_user_uid = table.Column<string>(type: "character varying(10)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_chara_data_allowance", x => new { x.parent_id, x.parent_uploader_uid, x.allowed_user_uid });
table.ForeignKey(
name: "fk_chara_data_allowance_chara_data_parent_id_parent_uploader_u",
columns: x => new { x.parent_id, x.parent_uploader_uid },
principalTable: "chara_data",
principalColumns: new[] { "id", "uploader_uid" },
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_chara_data_allowance_users_allowed_user_uid",
column: x => x.allowed_user_uid,
principalTable: "users",
principalColumn: "uid",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "chara_data_files",
columns: table => new
{
game_path = table.Column<string>(type: "text", nullable: false),
parent_id = table.Column<string>(type: "text", nullable: false),
file_cache_hash = table.Column<string>(type: "character varying(40)", nullable: true),
parent_uploader_uid = table.Column<string>(type: "character varying(10)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_chara_data_files", x => new { x.parent_id, x.game_path });
table.ForeignKey(
name: "fk_chara_data_files_chara_data_parent_id_parent_uploader_uid",
columns: x => new { x.parent_id, x.parent_uploader_uid },
principalTable: "chara_data",
principalColumns: new[] { "id", "uploader_uid" });
table.ForeignKey(
name: "fk_chara_data_files_files_file_cache_hash",
column: x => x.file_cache_hash,
principalTable: "file_caches",
principalColumn: "hash");
});
migrationBuilder.CreateTable(
name: "chara_data_orig_files",
columns: table => new
{
parent_id = table.Column<string>(type: "text", nullable: false),
parent_uploader_uid = table.Column<string>(type: "character varying(10)", nullable: false),
hash = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_chara_data_orig_files", x => new { x.parent_id, x.parent_uploader_uid, x.hash });
table.ForeignKey(
name: "fk_chara_data_orig_files_chara_data_parent_id_parent_uploader_",
columns: x => new { x.parent_id, x.parent_uploader_uid },
principalTable: "chara_data",
principalColumns: new[] { "id", "uploader_uid" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "chara_data_poses",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
parent_id = table.Column<string>(type: "text", nullable: false),
parent_uploader_uid = table.Column<string>(type: "character varying(10)", nullable: false),
description = table.Column<string>(type: "text", nullable: true),
pose_data = table.Column<string>(type: "text", nullable: true),
world_data = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_chara_data_poses", x => new { x.parent_id, x.parent_uploader_uid, x.id });
table.ForeignKey(
name: "fk_chara_data_poses_chara_data_parent_id_parent_uploader_uid",
columns: x => new { x.parent_id, x.parent_uploader_uid },
principalTable: "chara_data",
principalColumns: new[] { "id", "uploader_uid" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_chara_data_id",
table: "chara_data",
column: "id");
migrationBuilder.CreateIndex(
name: "ix_chara_data_uploader_uid",
table: "chara_data",
column: "uploader_uid");
migrationBuilder.CreateIndex(
name: "ix_chara_data_allowance_allowed_user_uid",
table: "chara_data_allowance",
column: "allowed_user_uid");
migrationBuilder.CreateIndex(
name: "ix_chara_data_allowance_parent_id",
table: "chara_data_allowance",
column: "parent_id");
migrationBuilder.CreateIndex(
name: "ix_chara_data_files_file_cache_hash",
table: "chara_data_files",
column: "file_cache_hash");
migrationBuilder.CreateIndex(
name: "ix_chara_data_files_parent_id",
table: "chara_data_files",
column: "parent_id");
migrationBuilder.CreateIndex(
name: "ix_chara_data_files_parent_id_parent_uploader_uid",
table: "chara_data_files",
columns: new[] { "parent_id", "parent_uploader_uid" });
migrationBuilder.CreateIndex(
name: "ix_chara_data_orig_files_parent_id",
table: "chara_data_orig_files",
column: "parent_id");
migrationBuilder.CreateIndex(
name: "ix_chara_data_poses_parent_id",
table: "chara_data_poses",
column: "parent_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "chara_data_allowance");
migrationBuilder.DropTable(
name: "chara_data_files");
migrationBuilder.DropTable(
name: "chara_data_orig_files");
migrationBuilder.DropTable(
name: "chara_data_poses");
migrationBuilder.DropTable(
name: "chara_data");
}
}
}

View File

@@ -0,0 +1,114 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MareSynchronosServer.Migrations
{
/// <inheritdoc />
public partial class CharaDataFileSwap : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_chara_data_files_chara_data_parent_id_parent_uploader_uid",
table: "chara_data_files");
migrationBuilder.DropPrimaryKey(
name: "pk_chara_data_files",
table: "chara_data_files");
migrationBuilder.DropIndex(
name: "ix_chara_data_files_parent_id_parent_uploader_uid",
table: "chara_data_files");
migrationBuilder.AlterColumn<string>(
name: "parent_uploader_uid",
table: "chara_data_files",
type: "character varying(10)",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "character varying(10)",
oldNullable: true);
migrationBuilder.AddPrimaryKey(
name: "pk_chara_data_files",
table: "chara_data_files",
columns: new[] { "parent_id", "parent_uploader_uid", "game_path" });
migrationBuilder.CreateTable(
name: "chara_data_file_swaps",
columns: table => new
{
parent_id = table.Column<string>(type: "text", nullable: false),
parent_uploader_uid = table.Column<string>(type: "character varying(10)", nullable: false),
game_path = table.Column<string>(type: "text", nullable: false),
file_path = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_chara_data_file_swaps", x => new { x.parent_id, x.parent_uploader_uid, x.game_path });
table.ForeignKey(
name: "fk_chara_data_file_swaps_chara_data_parent_id_parent_uploader_",
columns: x => new { x.parent_id, x.parent_uploader_uid },
principalTable: "chara_data",
principalColumns: new[] { "id", "uploader_uid" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_chara_data_file_swaps_parent_id",
table: "chara_data_file_swaps",
column: "parent_id");
migrationBuilder.AddForeignKey(
name: "fk_chara_data_files_chara_data_parent_id_parent_uploader_uid",
table: "chara_data_files",
columns: new[] { "parent_id", "parent_uploader_uid" },
principalTable: "chara_data",
principalColumns: new[] { "id", "uploader_uid" },
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_chara_data_files_chara_data_parent_id_parent_uploader_uid",
table: "chara_data_files");
migrationBuilder.DropTable(
name: "chara_data_file_swaps");
migrationBuilder.DropPrimaryKey(
name: "pk_chara_data_files",
table: "chara_data_files");
migrationBuilder.AlterColumn<string>(
name: "parent_uploader_uid",
table: "chara_data_files",
type: "character varying(10)",
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(10)");
migrationBuilder.AddPrimaryKey(
name: "pk_chara_data_files",
table: "chara_data_files",
columns: new[] { "parent_id", "game_path" });
migrationBuilder.CreateIndex(
name: "ix_chara_data_files_parent_id_parent_uploader_uid",
table: "chara_data_files",
columns: new[] { "parent_id", "parent_uploader_uid" });
migrationBuilder.AddForeignKey(
name: "fk_chara_data_files_chara_data_parent_id_parent_uploader_uid",
table: "chara_data_files",
columns: new[] { "parent_id", "parent_uploader_uid" },
principalTable: "chara_data",
principalColumns: new[] { "id", "uploader_uid" });
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MareSynchronosServer.Migrations
{
/// <inheritdoc />
public partial class CascadeFile : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_chara_data_files_files_file_cache_hash",
table: "chara_data_files");
migrationBuilder.AddForeignKey(
name: "fk_chara_data_files_files_file_cache_hash",
table: "chara_data_files",
column: "file_cache_hash",
principalTable: "file_caches",
principalColumn: "hash",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_chara_data_files_files_file_cache_hash",
table: "chara_data_files");
migrationBuilder.AddForeignKey(
name: "fk_chara_data_files_files_file_cache_hash",
table: "chara_data_files",
column: "file_cache_hash",
principalTable: "file_caches",
principalColumn: "hash");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,65 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MareSynchronosServer.Migrations
{
/// <inheritdoc />
public partial class OrigFileGamePath : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "pk_chara_data_orig_files",
table: "chara_data_orig_files");
migrationBuilder.AlterColumn<string>(
name: "hash",
table: "chara_data_orig_files",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AddColumn<string>(
name: "game_path",
table: "chara_data_orig_files",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddPrimaryKey(
name: "pk_chara_data_orig_files",
table: "chara_data_orig_files",
columns: new[] { "parent_id", "parent_uploader_uid", "game_path" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "pk_chara_data_orig_files",
table: "chara_data_orig_files");
migrationBuilder.DropColumn(
name: "game_path",
table: "chara_data_orig_files");
migrationBuilder.AlterColumn<string>(
name: "hash",
table: "chara_data_orig_files",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AddPrimaryKey(
name: "pk_chara_data_orig_files",
table: "chara_data_orig_files",
columns: new[] { "parent_id", "parent_uploader_uid", "hash" });
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MareSynchronosServer.Migrations
{
/// <inheritdoc />
public partial class ManipData : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "manipulation_data",
table: "chara_data",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "manipulation_data",
table: "chara_data");
}
}
}

View File

@@ -89,6 +89,216 @@ namespace MareSynchronosServer.Migrations
b.ToTable("banned_registrations", (string)null);
});
modelBuilder.Entity("MareSynchronosShared.Models.CharaData", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasColumnName("id");
b.Property<string>("UploaderUID")
.HasColumnType("character varying(10)")
.HasColumnName("uploader_uid");
b.Property<int>("AccessType")
.HasColumnType("integer")
.HasColumnName("access_type");
b.Property<DateTime>("CreatedDate")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_date");
b.Property<string>("CustomizeData")
.HasColumnType("text")
.HasColumnName("customize_data");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("description");
b.Property<int>("DownloadCount")
.HasColumnType("integer")
.HasColumnName("download_count");
b.Property<DateTime?>("ExpiryDate")
.HasColumnType("timestamp with time zone")
.HasColumnName("expiry_date");
b.Property<string>("GlamourerData")
.HasColumnType("text")
.HasColumnName("glamourer_data");
b.Property<string>("ManipulationData")
.HasColumnType("text")
.HasColumnName("manipulation_data");
b.Property<int>("ShareType")
.HasColumnType("integer")
.HasColumnName("share_type");
b.Property<DateTime>("UpdatedDate")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_date");
b.HasKey("Id", "UploaderUID")
.HasName("pk_chara_data");
b.HasIndex("Id")
.HasDatabaseName("ix_chara_data_id");
b.HasIndex("UploaderUID")
.HasDatabaseName("ix_chara_data_uploader_uid");
b.ToTable("chara_data", (string)null);
});
modelBuilder.Entity("MareSynchronosShared.Models.CharaDataAllowance", b =>
{
b.Property<string>("ParentId")
.HasColumnType("text")
.HasColumnName("parent_id");
b.Property<string>("ParentUploaderUID")
.HasColumnType("character varying(10)")
.HasColumnName("parent_uploader_uid");
b.Property<string>("AllowedUserUID")
.HasColumnType("character varying(10)")
.HasColumnName("allowed_user_uid");
b.HasKey("ParentId", "ParentUploaderUID", "AllowedUserUID")
.HasName("pk_chara_data_allowance");
b.HasIndex("AllowedUserUID")
.HasDatabaseName("ix_chara_data_allowance_allowed_user_uid");
b.HasIndex("ParentId")
.HasDatabaseName("ix_chara_data_allowance_parent_id");
b.ToTable("chara_data_allowance", (string)null);
});
modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFile", b =>
{
b.Property<string>("ParentId")
.HasColumnType("text")
.HasColumnName("parent_id");
b.Property<string>("ParentUploaderUID")
.HasColumnType("character varying(10)")
.HasColumnName("parent_uploader_uid");
b.Property<string>("GamePath")
.HasColumnType("text")
.HasColumnName("game_path");
b.Property<string>("FileCacheHash")
.HasColumnType("character varying(40)")
.HasColumnName("file_cache_hash");
b.HasKey("ParentId", "ParentUploaderUID", "GamePath")
.HasName("pk_chara_data_files");
b.HasIndex("FileCacheHash")
.HasDatabaseName("ix_chara_data_files_file_cache_hash");
b.HasIndex("ParentId")
.HasDatabaseName("ix_chara_data_files_parent_id");
b.ToTable("chara_data_files", (string)null);
});
modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFileSwap", b =>
{
b.Property<string>("ParentId")
.HasColumnType("text")
.HasColumnName("parent_id");
b.Property<string>("ParentUploaderUID")
.HasColumnType("character varying(10)")
.HasColumnName("parent_uploader_uid");
b.Property<string>("GamePath")
.HasColumnType("text")
.HasColumnName("game_path");
b.Property<string>("FilePath")
.HasColumnType("text")
.HasColumnName("file_path");
b.HasKey("ParentId", "ParentUploaderUID", "GamePath")
.HasName("pk_chara_data_file_swaps");
b.HasIndex("ParentId")
.HasDatabaseName("ix_chara_data_file_swaps_parent_id");
b.ToTable("chara_data_file_swaps", (string)null);
});
modelBuilder.Entity("MareSynchronosShared.Models.CharaDataOriginalFile", b =>
{
b.Property<string>("ParentId")
.HasColumnType("text")
.HasColumnName("parent_id");
b.Property<string>("ParentUploaderUID")
.HasColumnType("character varying(10)")
.HasColumnName("parent_uploader_uid");
b.Property<string>("GamePath")
.HasColumnType("text")
.HasColumnName("game_path");
b.Property<string>("Hash")
.HasColumnType("text")
.HasColumnName("hash");
b.HasKey("ParentId", "ParentUploaderUID", "GamePath")
.HasName("pk_chara_data_orig_files");
b.HasIndex("ParentId")
.HasDatabaseName("ix_chara_data_orig_files_parent_id");
b.ToTable("chara_data_orig_files", (string)null);
});
modelBuilder.Entity("MareSynchronosShared.Models.CharaDataPose", b =>
{
b.Property<string>("ParentId")
.HasColumnType("text")
.HasColumnName("parent_id");
b.Property<string>("ParentUploaderUID")
.HasColumnType("character varying(10)")
.HasColumnName("parent_uploader_uid");
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("description");
b.Property<string>("PoseData")
.HasColumnType("text")
.HasColumnName("pose_data");
b.Property<string>("WorldData")
.HasColumnType("text")
.HasColumnName("world_data");
b.HasKey("ParentId", "ParentUploaderUID", "Id")
.HasName("pk_chara_data_poses");
b.HasIndex("ParentId")
.HasDatabaseName("ix_chara_data_poses_parent_id");
b.ToTable("chara_data_poses", (string)null);
});
modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b =>
{
b.Property<string>("UserUID")
@@ -513,6 +723,95 @@ namespace MareSynchronosServer.Migrations
b.Navigation("User");
});
modelBuilder.Entity("MareSynchronosShared.Models.CharaData", b =>
{
b.HasOne("MareSynchronosShared.Models.User", "Uploader")
.WithMany()
.HasForeignKey("UploaderUID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_chara_data_users_uploader_uid");
b.Navigation("Uploader");
});
modelBuilder.Entity("MareSynchronosShared.Models.CharaDataAllowance", b =>
{
b.HasOne("MareSynchronosShared.Models.User", "AllowedUser")
.WithMany()
.HasForeignKey("AllowedUserUID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid");
b.HasOne("MareSynchronosShared.Models.CharaData", "Parent")
.WithMany("AllowedIndividiuals")
.HasForeignKey("ParentId", "ParentUploaderUID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_chara_data_allowance_chara_data_parent_id_parent_uploader_u");
b.Navigation("AllowedUser");
b.Navigation("Parent");
});
modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFile", b =>
{
b.HasOne("MareSynchronosShared.Models.FileCache", "FileCache")
.WithMany()
.HasForeignKey("FileCacheHash")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_chara_data_files_files_file_cache_hash");
b.HasOne("MareSynchronosShared.Models.CharaData", "Parent")
.WithMany("Files")
.HasForeignKey("ParentId", "ParentUploaderUID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_chara_data_files_chara_data_parent_id_parent_uploader_uid");
b.Navigation("FileCache");
b.Navigation("Parent");
});
modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFileSwap", b =>
{
b.HasOne("MareSynchronosShared.Models.CharaData", "Parent")
.WithMany("FileSwaps")
.HasForeignKey("ParentId", "ParentUploaderUID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_chara_data_file_swaps_chara_data_parent_id_parent_uploader_");
b.Navigation("Parent");
});
modelBuilder.Entity("MareSynchronosShared.Models.CharaDataOriginalFile", b =>
{
b.HasOne("MareSynchronosShared.Models.CharaData", "Parent")
.WithMany("OriginalFiles")
.HasForeignKey("ParentId", "ParentUploaderUID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_chara_data_orig_files_chara_data_parent_id_parent_uploader_");
b.Navigation("Parent");
});
modelBuilder.Entity("MareSynchronosShared.Models.CharaDataPose", b =>
{
b.HasOne("MareSynchronosShared.Models.CharaData", "Parent")
.WithMany("Poses")
.HasForeignKey("ParentId", "ParentUploaderUID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_chara_data_poses_chara_data_parent_id_parent_uploader_uid");
b.Navigation("Parent");
});
modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b =>
{
b.HasOne("MareSynchronosShared.Models.User", "OtherUser")
@@ -653,6 +952,19 @@ namespace MareSynchronosServer.Migrations
b.Navigation("ReportingUser");
});
modelBuilder.Entity("MareSynchronosShared.Models.CharaData", b =>
{
b.Navigation("AllowedIndividiuals");
b.Navigation("FileSwaps");
b.Navigation("Files");
b.Navigation("OriginalFiles");
b.Navigation("Poses");
});
#pragma warning restore 612, 618
}
}

View File

@@ -0,0 +1,87 @@
using System.ComponentModel.DataAnnotations;
namespace MareSynchronosShared.Models;
public enum CharaDataAccess
{
Individuals,
ClosePairs,
AllPairs,
Public
}
public enum CharaDataShare
{
Private,
Shared
}
public class CharaData
{
public string Id { get; set; }
public virtual User Uploader { get; set; }
public string UploaderUID { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime UpdatedDate { get; set; }
public string Description { get; set; }
public CharaDataAccess AccessType { get; set; }
public CharaDataShare ShareType { get; set; }
public DateTime? ExpiryDate { get; set; }
public string? GlamourerData { get; set; }
public string? CustomizeData { get; set; }
public string? ManipulationData { get; set; }
public int DownloadCount { get; set; } = 0;
public virtual ICollection<CharaDataPose> Poses { get; set; } = [];
public virtual ICollection<CharaDataFile> Files { get; set; } = [];
public virtual ICollection<CharaDataFileSwap> FileSwaps { get; set; } = [];
public virtual ICollection<CharaDataOriginalFile> OriginalFiles { get; set; } = [];
public virtual ICollection<CharaDataAllowance> AllowedIndividiuals { get; set; } = [];
}
public class CharaDataAllowance
{
public virtual CharaData Parent { get; set; }
public string ParentId { get; set; }
public string ParentUploaderUID { get; set; }
public virtual User AllowedUser { get; set; }
public string AllowedUserUID { get; set; }
}
public class CharaDataOriginalFile
{
public virtual CharaData Parent { get; set; }
public string ParentId { get; set; }
public string ParentUploaderUID { get; set; }
public string GamePath { get; set; }
public string Hash { get; set; }
}
public class CharaDataFile
{
public virtual FileCache FileCache { get; set; }
public string FileCacheHash { get; set; }
public string GamePath { get; set; }
public virtual CharaData Parent { get; set; }
public string ParentId { get; set; }
public string ParentUploaderUID { get; set; }
}
public class CharaDataFileSwap
{
public virtual CharaData Parent { get; set; }
public string ParentId { get; set; }
public string ParentUploaderUID { get; set; }
public string GamePath { get; set; }
public string FilePath { get; set; }
}
public class CharaDataPose
{
public long Id { get; set; }
public virtual CharaData Parent { get; set; }
public string ParentId { get; set; }
public string ParentUploaderUID { get; set; }
public string Description { get; set; }
public string PoseData { get; set; }
public string WorldData { get; set; }
}

View File

@@ -25,6 +25,9 @@ public class ServerConfiguration : MareConfigurationBase
[RemoteConfiguration]
public int PurgeUnusedAccountsPeriodInDays { get; set; } = 14;
[RemoteConfiguration]
public int MaxCharaDataByUser { get; set; } = 10;
public override string ToString()
{
StringBuilder sb = new();
@@ -37,6 +40,7 @@ public class ServerConfiguration : MareConfigurationBase
sb.AppendLine($"{nameof(MaxGroupUserCount)} => {MaxGroupUserCount}");
sb.AppendLine($"{nameof(PurgeUnusedAccounts)} => {PurgeUnusedAccounts}");
sb.AppendLine($"{nameof(PurgeUnusedAccountsPeriodInDays)} => {PurgeUnusedAccountsPeriodInDays}");
sb.AppendLine($"{nameof(MaxCharaDataByUser)} => {MaxCharaDataByUser}");
return sb.ToString();
}
}