♻️ Refactor cache system with redis
🐛 Add lock to check in prevent multiple at the same time
This commit is contained in:
parent
d4da5d7afc
commit
460ce62452
@ -344,7 +344,7 @@ public class AccountController(
|
|||||||
if (!isAvailable)
|
if (!isAvailable)
|
||||||
return BadRequest("Check-in is not available for today.");
|
return BadRequest("Check-in is not available for today.");
|
||||||
|
|
||||||
var needsCaptcha = events.CheckInDailyDoAskCaptcha(currentUser);
|
var needsCaptcha = await events.CheckInDailyDoAskCaptcha(currentUser);
|
||||||
return needsCaptcha switch
|
return needsCaptcha switch
|
||||||
{
|
{
|
||||||
true when string.IsNullOrWhiteSpace(captchaToken) => StatusCode(423,
|
true when string.IsNullOrWhiteSpace(captchaToken) => StatusCode(423,
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using DysonNetwork.Sphere.Activity;
|
using DysonNetwork.Sphere.Activity;
|
||||||
using DysonNetwork.Sphere.Connection;
|
using DysonNetwork.Sphere.Connection;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
using DysonNetwork.Sphere.Wallet;
|
using DysonNetwork.Sphere.Wallet;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using Org.BouncyCastle.Asn1.X509;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
@ -13,24 +15,25 @@ public class AccountEventService(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
ActivityService act,
|
ActivityService act,
|
||||||
WebSocketService ws,
|
WebSocketService ws,
|
||||||
IMemoryCache cache,
|
ICacheService cache,
|
||||||
PaymentService payment,
|
PaymentService payment,
|
||||||
IStringLocalizer<Localization.AccountEventResource> localizer
|
IStringLocalizer<Localization.AccountEventResource> localizer
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
private static readonly Random Random = new();
|
private static readonly Random Random = new();
|
||||||
private const string StatusCacheKey = "account_status_";
|
private const string StatusCacheKey = "AccountStatus_";
|
||||||
|
|
||||||
public void PurgeStatusCache(Guid userId)
|
public void PurgeStatusCache(Guid userId)
|
||||||
{
|
{
|
||||||
var cacheKey = $"{StatusCacheKey}{userId}";
|
var cacheKey = $"{StatusCacheKey}{userId}";
|
||||||
cache.Remove(cacheKey);
|
cache.RemoveAsync(cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Status> GetStatus(Guid userId)
|
public async Task<Status> GetStatus(Guid userId)
|
||||||
{
|
{
|
||||||
var cacheKey = $"{StatusCacheKey}{userId}";
|
var cacheKey = $"{StatusCacheKey}{userId}";
|
||||||
if (cache.TryGetValue(cacheKey, out Status? cachedStatus))
|
var cachedStatus = await cache.GetAsync<Status>(cacheKey);
|
||||||
|
if (cachedStatus is not null)
|
||||||
{
|
{
|
||||||
cachedStatus!.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId);
|
cachedStatus!.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId);
|
||||||
return cachedStatus;
|
return cachedStatus;
|
||||||
@ -46,7 +49,8 @@ public class AccountEventService(
|
|||||||
if (status is not null)
|
if (status is not null)
|
||||||
{
|
{
|
||||||
status.IsOnline = !status.IsInvisible && isOnline;
|
status.IsOnline = !status.IsInvisible && isOnline;
|
||||||
cache.Set(cacheKey, status, TimeSpan.FromMinutes(5));
|
await cache.SetWithGroupsAsync(cacheKey, status, [$"{AccountService.AccountCachePrefix}{status.AccountId}"],
|
||||||
|
TimeSpan.FromMinutes(5));
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,17 +105,18 @@ public class AccountEventService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const int FortuneTipCount = 7; // This will be the max index for each type (positive/negative)
|
private const int FortuneTipCount = 7; // This will be the max index for each type (positive/negative)
|
||||||
private const string CaptchaCacheKey = "checkin_captcha_";
|
private const string CaptchaCacheKey = "CheckInCaptcha_";
|
||||||
private const int CaptchaProbabilityPercent = 20;
|
private const int CaptchaProbabilityPercent = 20;
|
||||||
|
|
||||||
public bool CheckInDailyDoAskCaptcha(Account user)
|
public async Task<bool> CheckInDailyDoAskCaptcha(Account user)
|
||||||
{
|
{
|
||||||
var cacheKey = $"{CaptchaCacheKey}{user.Id}";
|
var cacheKey = $"{CaptchaCacheKey}{user.Id}";
|
||||||
if (cache.TryGetValue(cacheKey, out bool? needsCaptcha))
|
var needsCaptcha = await cache.GetAsync<bool?>(cacheKey);
|
||||||
|
if (needsCaptcha is not null)
|
||||||
return needsCaptcha!.Value;
|
return needsCaptcha!.Value;
|
||||||
|
|
||||||
var result = Random.Next(100) < CaptchaProbabilityPercent;
|
var result = Random.Next(100) < CaptchaProbabilityPercent;
|
||||||
cache.Set(cacheKey, result, TimeSpan.FromHours(24));
|
await cache.SetAsync(cacheKey, result, TimeSpan.FromHours(24));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,8 +137,14 @@ public class AccountEventService(
|
|||||||
return lastDate < currentDate;
|
return lastDate < currentDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public const string CheckInLockKey = "CheckInLock_";
|
||||||
|
|
||||||
public async Task<CheckInResult> CheckInDaily(Account user)
|
public async Task<CheckInResult> CheckInDaily(Account user)
|
||||||
{
|
{
|
||||||
|
var lockKey = $"{CheckInLockKey}{user.Id}";
|
||||||
|
var lk = await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(10), TimeSpan.Zero);
|
||||||
|
if (lk is null) throw new InvalidOperationException("Check-in was in progress.");
|
||||||
|
|
||||||
var cultureInfo = new CultureInfo(user.Language, false);
|
var cultureInfo = new CultureInfo(user.Language, false);
|
||||||
CultureInfo.CurrentCulture = cultureInfo;
|
CultureInfo.CurrentCulture = cultureInfo;
|
||||||
CultureInfo.CurrentUICulture = cultureInfo;
|
CultureInfo.CurrentUICulture = cultureInfo;
|
||||||
@ -201,6 +212,7 @@ public class AccountEventService(
|
|||||||
ActivityVisibility.Friends
|
ActivityVisibility.Friends
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await lk.ReleaseAsync();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ using System.Globalization;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
using DysonNetwork.Sphere.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
using EFCore.BulkExtensions;
|
using EFCore.BulkExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
@ -12,20 +13,15 @@ namespace DysonNetwork.Sphere.Account;
|
|||||||
|
|
||||||
public class AccountService(
|
public class AccountService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
IMemoryCache cache,
|
ICacheService cache,
|
||||||
IStringLocalizerFactory factory
|
IStringLocalizerFactory factory
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
public const string AccountCachePrefix = "Account_";
|
||||||
|
|
||||||
public async Task PurgeAccountCache(Account account)
|
public async Task PurgeAccountCache(Account account)
|
||||||
{
|
{
|
||||||
cache.Remove($"UserFriends_{account.Id}");
|
await cache.RemoveGroupAsync($"{AccountCachePrefix}{account.Id}");
|
||||||
|
|
||||||
var sessions = await db.AuthSessions.Where(e => e.Account.Id == account.Id).Select(e => e.Id)
|
|
||||||
.ToListAsync();
|
|
||||||
foreach (var session in sessions)
|
|
||||||
{
|
|
||||||
cache.Remove($"Auth_{session}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Account?> LookupAccount(string probe)
|
public async Task<Account?> LookupAccount(string probe)
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
public class RelationshipService(AppDatabase db, IMemoryCache cache)
|
public class RelationshipService(AppDatabase db, ICacheService cache)
|
||||||
{
|
{
|
||||||
|
private const string UserFriendsCacheKeyPrefix = "UserFriends_";
|
||||||
|
|
||||||
public async Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId)
|
public async Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId)
|
||||||
{
|
{
|
||||||
var count = await db.AccountRelationships
|
var count = await db.AccountRelationships
|
||||||
@ -49,8 +51,8 @@ public class RelationshipService(AppDatabase db, IMemoryCache cache)
|
|||||||
db.AccountRelationships.Add(relationship);
|
db.AccountRelationships.Add(relationship);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
cache.Remove($"UserFriends_{relationship.AccountId}");
|
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relationship.AccountId}");
|
||||||
cache.Remove($"UserFriends_{relationship.RelatedId}");
|
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relationship.RelatedId}");
|
||||||
|
|
||||||
return relationship;
|
return relationship;
|
||||||
}
|
}
|
||||||
@ -89,8 +91,8 @@ public class RelationshipService(AppDatabase db, IMemoryCache cache)
|
|||||||
db.AccountRelationships.Remove(relationship);
|
db.AccountRelationships.Remove(relationship);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
cache.Remove($"UserFriends_{accountId}");
|
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{accountId}");
|
||||||
cache.Remove($"UserFriends_{relatedId}");
|
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relatedId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Relationship> AcceptFriendRelationship(
|
public async Task<Relationship> AcceptFriendRelationship(
|
||||||
@ -119,8 +121,8 @@ public class RelationshipService(AppDatabase db, IMemoryCache cache)
|
|||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
cache.Remove($"UserFriends_{relationship.AccountId}");
|
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relationship.AccountId}");
|
||||||
cache.Remove($"UserFriends_{relationship.RelatedId}");
|
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relationship.RelatedId}");
|
||||||
|
|
||||||
return relationshipBackward;
|
return relationshipBackward;
|
||||||
}
|
}
|
||||||
@ -133,21 +135,27 @@ public class RelationshipService(AppDatabase db, IMemoryCache cache)
|
|||||||
relationship.Status = status;
|
relationship.Status = status;
|
||||||
db.Update(relationship);
|
db.Update(relationship);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
cache.Remove($"UserFriends_{accountId}");
|
|
||||||
cache.Remove($"UserFriends_{relatedId}");
|
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{accountId}");
|
||||||
|
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relatedId}");
|
||||||
|
|
||||||
return relationship;
|
return relationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Guid>> ListAccountFriends(Account account)
|
public async Task<List<Guid>> ListAccountFriends(Account account)
|
||||||
{
|
{
|
||||||
if (!cache.TryGetValue($"UserFriends_{account.Id}", out List<Guid>? friends))
|
string cacheKey = $"{UserFriendsCacheKeyPrefix}{account.Id}";
|
||||||
|
var friends = await cache.GetAsync<List<Guid>>(cacheKey);
|
||||||
|
|
||||||
|
if (friends == null)
|
||||||
{
|
{
|
||||||
friends = await db.AccountRelationships
|
friends = await db.AccountRelationships
|
||||||
.Where(r => r.RelatedId == account.Id)
|
.Where(r => r.RelatedId == account.Id)
|
||||||
.Where(r => r.Status == RelationshipStatus.Friends)
|
.Where(r => r.Status == RelationshipStatus.Friends)
|
||||||
.Select(r => r.AccountId)
|
.Select(r => r.AccountId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
cache.Set($"UserFriends_{account.Id}", friends, TimeSpan.FromHours(1));
|
|
||||||
|
await cache.SetAsync(cacheKey, friends, TimeSpan.FromHours(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
return friends ?? [];
|
return friends ?? [];
|
||||||
|
@ -1,27 +1,30 @@
|
|||||||
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth;
|
namespace DysonNetwork.Sphere.Auth;
|
||||||
|
|
||||||
public class UserInfoMiddleware(RequestDelegate next, IMemoryCache cache)
|
public class UserInfoMiddleware(RequestDelegate next, ICacheService cache)
|
||||||
{
|
{
|
||||||
public async Task InvokeAsync(HttpContext context, AppDatabase db)
|
public async Task InvokeAsync(HttpContext context, AppDatabase db)
|
||||||
{
|
{
|
||||||
var sessionIdClaim = context.User.FindFirst("session_id")?.Value;
|
var sessionIdClaim = context.User.FindFirst("session_id")?.Value;
|
||||||
if (sessionIdClaim is not null && Guid.TryParse(sessionIdClaim, out var sessionId))
|
if (sessionIdClaim is not null && Guid.TryParse(sessionIdClaim, out var sessionId))
|
||||||
{
|
{
|
||||||
if (!cache.TryGetValue($"Auth_{sessionId}", out Session? session))
|
var session = await cache.GetAsync<Session>($"Auth_{sessionId}");
|
||||||
|
if (session is null)
|
||||||
{
|
{
|
||||||
session = await db.AuthSessions
|
session = await db.AuthSessions
|
||||||
|
.Where(e => e.Id == sessionId)
|
||||||
.Include(e => e.Challenge)
|
.Include(e => e.Challenge)
|
||||||
.Include(e => e.Account)
|
.Include(e => e.Account)
|
||||||
.Include(e => e.Account.Profile)
|
.ThenInclude(e => e.Profile)
|
||||||
.Where(e => e.Id == sessionId)
|
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (session is not null)
|
if (session is not null)
|
||||||
{
|
{
|
||||||
cache.Set($"Auth_{sessionId}", session, TimeSpan.FromHours(1));
|
await cache.SetWithGroupsAsync($"Auth_{sessionId}", session,
|
||||||
|
[$"{AccountService.AccountCachePrefix}{session.Account.Id}"], TimeSpan.FromHours(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,37 +1,40 @@
|
|||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat;
|
namespace DysonNetwork.Sphere.Chat;
|
||||||
|
|
||||||
public class ChatRoomService(AppDatabase db, IMemoryCache cache)
|
public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||||
{
|
{
|
||||||
private const string RoomMembersCacheKey = "ChatRoomMembers_{0}";
|
public const string ChatRoomGroupPrefix = "ChatRoom_";
|
||||||
|
private const string RoomMembersCacheKeyPrefix = "ChatRoomMembers_";
|
||||||
|
|
||||||
public async Task<List<ChatMember>> ListRoomMembers(Guid roomId)
|
public async Task<List<ChatMember>> ListRoomMembers(Guid roomId)
|
||||||
{
|
{
|
||||||
var cacheKey = string.Format(RoomMembersCacheKey, roomId);
|
var cacheKey = RoomMembersCacheKeyPrefix + roomId;
|
||||||
if (cache.TryGetValue(cacheKey, out List<ChatMember>? cachedMembers))
|
var cachedMembers = await cache.GetAsync<List<ChatMember>>(cacheKey);
|
||||||
return cachedMembers!;
|
if (cachedMembers != null)
|
||||||
|
return cachedMembers;
|
||||||
|
|
||||||
var members = await db.ChatMembers
|
var members = await db.ChatMembers
|
||||||
.Where(m => m.ChatRoomId == roomId)
|
.Where(m => m.ChatRoomId == roomId)
|
||||||
.Where(m => m.JoinedAt != null)
|
.Where(m => m.JoinedAt != null)
|
||||||
.Where(m => m.LeaveAt == null)
|
.Where(m => m.LeaveAt == null)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var cacheOptions = new MemoryCacheEntryOptions()
|
var chatRoomGroup = ChatRoomGroupPrefix + roomId;
|
||||||
.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
|
await cache.SetWithGroupsAsync(cacheKey, members,
|
||||||
cache.Set(cacheKey, members, cacheOptions);
|
new[] { chatRoomGroup },
|
||||||
|
TimeSpan.FromMinutes(5));
|
||||||
|
|
||||||
return members;
|
return members;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PurgeRoomMembersCache(Guid roomId)
|
public async Task PurgeRoomMembersCache(Guid roomId)
|
||||||
{
|
{
|
||||||
var cacheKey = string.Format(RoomMembersCacheKey, roomId);
|
var chatRoomGroup = ChatRoomGroupPrefix + roomId;
|
||||||
cache.Remove(cacheKey);
|
await cache.RemoveGroupAsync(chatRoomGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<ChatRoom>> SortChatRoomByLastMessage(List<ChatRoom> rooms)
|
public async Task<List<ChatRoom>> SortChatRoomByLastMessage(List<ChatRoom> rooms)
|
||||||
|
@ -10,7 +10,7 @@ namespace DysonNetwork.Sphere.Connection.Handlers;
|
|||||||
|
|
||||||
public class MessageReadHandler(
|
public class MessageReadHandler(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
IMemoryCache cache,
|
ICacheService cache,
|
||||||
ChatRoomService crs,
|
ChatRoomService crs,
|
||||||
FlushBufferService buffer
|
FlushBufferService buffer
|
||||||
)
|
)
|
||||||
@ -44,11 +44,9 @@ public class MessageReadHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatMember? sender;
|
|
||||||
var cacheKey = string.Format(ChatMemberCacheKey, currentUser.Id, request.ChatRoomId);
|
var cacheKey = string.Format(ChatMemberCacheKey, currentUser.Id, request.ChatRoomId);
|
||||||
if (cache.TryGetValue(cacheKey, out ChatMember? cachedMember))
|
var sender = await cache.GetAsync<ChatMember?>(cacheKey);
|
||||||
sender = cachedMember;
|
if (sender is null)
|
||||||
else
|
|
||||||
{
|
{
|
||||||
sender = await db.ChatMembers
|
sender = await db.ChatMembers
|
||||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == request.ChatRoomId)
|
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == request.ChatRoomId)
|
||||||
@ -56,9 +54,10 @@ public class MessageReadHandler(
|
|||||||
|
|
||||||
if (sender != null)
|
if (sender != null)
|
||||||
{
|
{
|
||||||
var cacheOptions = new MemoryCacheEntryOptions()
|
var chatRoomGroup = ChatRoomService.ChatRoomGroupPrefix + request.ChatRoomId;
|
||||||
.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
|
await cache.SetWithGroupsAsync(cacheKey, sender,
|
||||||
cache.Set(cacheKey, sender, cacheOptions);
|
[chatRoomGroup],
|
||||||
|
TimeSpan.FromMinutes(5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using DysonNetwork.Sphere.Chat;
|
using DysonNetwork.Sphere.Chat;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Connection.Handlers;
|
namespace DysonNetwork.Sphere.Connection.Handlers;
|
||||||
|
|
||||||
public class MessageTypingHandler(AppDatabase db, ChatRoomService crs, IMemoryCache cache) : IWebSocketPacketHandler
|
public class MessageTypingHandler(AppDatabase db, ChatRoomService crs, ICacheService cache) : IWebSocketPacketHandler
|
||||||
{
|
{
|
||||||
public string PacketType => "messages.typing";
|
public string PacketType => "messages.typing";
|
||||||
|
|
||||||
@ -33,11 +34,9 @@ public class MessageTypingHandler(AppDatabase db, ChatRoomService crs, IMemoryCa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatMember? sender = null;
|
|
||||||
var cacheKey = string.Format(MessageReadHandler.ChatMemberCacheKey, currentUser.Id, request.ChatRoomId);
|
var cacheKey = string.Format(MessageReadHandler.ChatMemberCacheKey, currentUser.Id, request.ChatRoomId);
|
||||||
if (cache.TryGetValue(cacheKey, out ChatMember? cachedMember))
|
var sender = await cache.GetAsync<ChatMember?>(cacheKey);
|
||||||
sender = cachedMember;
|
if (sender is null)
|
||||||
else
|
|
||||||
{
|
{
|
||||||
sender = await db.ChatMembers
|
sender = await db.ChatMembers
|
||||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == request.ChatRoomId)
|
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == request.ChatRoomId)
|
||||||
@ -45,9 +44,10 @@ public class MessageTypingHandler(AppDatabase db, ChatRoomService crs, IMemoryCa
|
|||||||
|
|
||||||
if (sender != null)
|
if (sender != null)
|
||||||
{
|
{
|
||||||
var cacheOptions = new MemoryCacheEntryOptions()
|
var chatRoomGroup = ChatRoomService.ChatRoomGroupPrefix + request.ChatRoomId;
|
||||||
.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
|
await cache.SetWithGroupsAsync(cacheKey, sender,
|
||||||
cache.Set(cacheKey, sender, cacheOptions);
|
[chatRoomGroup],
|
||||||
|
TimeSpan.FromMinutes(5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +53,8 @@
|
|||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.9" />
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.9" />
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.macOS" Version="2.88.9" />
|
<PackageReference Include="SkiaSharp.NativeAssets.macOS" Version="2.88.9" />
|
||||||
|
<PackageReference Include="StackExchange.Redis" Version="2.8.37" />
|
||||||
|
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0"/>
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0"/>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="8.1.0"/>
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="8.1.0"/>
|
||||||
<PackageReference Include="tusdotnet" Version="2.8.1"/>
|
<PackageReference Include="tusdotnet" Version="2.8.1"/>
|
||||||
|
@ -1,22 +1,29 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using System.Text.Json;
|
||||||
|
using DysonNetwork.Sphere.Storage;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Permission;
|
namespace DysonNetwork.Sphere.Permission;
|
||||||
|
|
||||||
public class PermissionService(
|
public class PermissionService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
IMemoryCache cache,
|
ICacheService cache,
|
||||||
ILogger<PermissionService> logger)
|
ILogger<PermissionService> logger)
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan CacheExpiration = TimeSpan.FromMinutes(1);
|
private static readonly TimeSpan CacheExpiration = TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
|
private const string PermCacheKeyPrefix = "Perm_";
|
||||||
|
private const string PermGroupCacheKeyPrefix = "PermCacheGroup_";
|
||||||
|
private const string PermissionGroupPrefix = "PermGroup_";
|
||||||
|
|
||||||
private string GetPermissionCacheKey(string actor, string area, string key) =>
|
private static string _GetPermissionCacheKey(string actor, string area, string key) =>
|
||||||
$"perm:{actor}:{area}:{key}";
|
PermCacheKeyPrefix + actor + ":" + area + ":" + key;
|
||||||
|
|
||||||
private string GetGroupsCacheKey(string actor) =>
|
private static string _GetGroupsCacheKey(string actor) =>
|
||||||
$"perm_groups:{actor}";
|
PermGroupCacheKeyPrefix + actor;
|
||||||
|
|
||||||
|
private static string _GetPermissionGroupKey(string actor) =>
|
||||||
|
PermissionGroupPrefix + actor;
|
||||||
|
|
||||||
public async Task<bool> HasPermissionAsync(string actor, string area, string key)
|
public async Task<bool> HasPermissionAsync(string actor, string area, string key)
|
||||||
{
|
{
|
||||||
@ -26,26 +33,31 @@ public class PermissionService(
|
|||||||
|
|
||||||
public async Task<T?> GetPermissionAsync<T>(string actor, string area, string key)
|
public async Task<T?> GetPermissionAsync<T>(string actor, string area, string key)
|
||||||
{
|
{
|
||||||
var cacheKey = GetPermissionCacheKey(actor, area, key);
|
var cacheKey = _GetPermissionCacheKey(actor, area, key);
|
||||||
|
|
||||||
if (cache.TryGetValue<T>(cacheKey, out var cachedValue))
|
var cachedValue = await cache.GetAsync<T>(cacheKey);
|
||||||
|
if (cachedValue != null)
|
||||||
{
|
{
|
||||||
return cachedValue;
|
return cachedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
var groupsKey = GetGroupsCacheKey(actor);
|
var groupsKey = _GetGroupsCacheKey(actor);
|
||||||
|
|
||||||
var groupsId = await cache.GetOrCreateAsync(groupsKey, async entry =>
|
var groupsId = await cache.GetAsync<List<Guid>>(groupsKey);
|
||||||
|
if (groupsId == null)
|
||||||
{
|
{
|
||||||
entry.AbsoluteExpirationRelativeToNow = CacheExpiration;
|
groupsId = await db.PermissionGroupMembers
|
||||||
return await db.PermissionGroupMembers
|
|
||||||
.Where(n => n.Actor == actor)
|
.Where(n => n.Actor == actor)
|
||||||
.Where(n => n.ExpiredAt == null || n.ExpiredAt < now)
|
.Where(n => n.ExpiredAt == null || n.ExpiredAt < now)
|
||||||
.Where(n => n.AffectedAt == null || n.AffectedAt >= now)
|
.Where(n => n.AffectedAt == null || n.AffectedAt >= now)
|
||||||
.Select(e => e.GroupId)
|
.Select(e => e.GroupId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
});
|
|
||||||
|
await cache.SetWithGroupsAsync(groupsKey, groupsId,
|
||||||
|
new[] { _GetPermissionGroupKey(actor) },
|
||||||
|
CacheExpiration);
|
||||||
|
}
|
||||||
|
|
||||||
var permission = await db.PermissionNodes
|
var permission = await db.PermissionNodes
|
||||||
.Where(n => n.GroupId == null || groupsId.Contains(n.GroupId.Value))
|
.Where(n => n.GroupId == null || groupsId.Contains(n.GroupId.Value))
|
||||||
@ -56,7 +68,9 @@ public class PermissionService(
|
|||||||
|
|
||||||
var result = permission is not null ? _DeserializePermissionValue<T>(permission.Value) : default;
|
var result = permission is not null ? _DeserializePermissionValue<T>(permission.Value) : default;
|
||||||
|
|
||||||
cache.Set(cacheKey, result, CacheExpiration);
|
await cache.SetWithGroupsAsync(cacheKey, result,
|
||||||
|
new[] { _GetPermissionGroupKey(actor) },
|
||||||
|
CacheExpiration);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -86,7 +100,7 @@ public class PermissionService(
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// Invalidate related caches
|
// Invalidate related caches
|
||||||
InvalidatePermissionCache(actor, area, key);
|
await InvalidatePermissionCacheAsync(actor, area, key);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
@ -119,8 +133,9 @@ public class PermissionService(
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// Invalidate related caches
|
// Invalidate related caches
|
||||||
InvalidatePermissionCache(actor, area, key);
|
await InvalidatePermissionCacheAsync(actor, area, key);
|
||||||
cache.Remove(GetGroupsCacheKey(actor));
|
await cache.RemoveAsync(_GetGroupsCacheKey(actor));
|
||||||
|
await cache.RemoveGroupAsync(_GetPermissionGroupKey(actor));
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
@ -134,7 +149,7 @@ public class PermissionService(
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// Invalidate cache
|
// Invalidate cache
|
||||||
InvalidatePermissionCache(actor, area, key);
|
await InvalidatePermissionCacheAsync(actor, area, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemovePermissionNodeFromGroup<T>(PermissionGroup group, string actor, string area, string key)
|
public async Task RemovePermissionNodeFromGroup<T>(PermissionGroup group, string actor, string area, string key)
|
||||||
@ -148,14 +163,15 @@ public class PermissionService(
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// Invalidate caches
|
// Invalidate caches
|
||||||
InvalidatePermissionCache(actor, area, key);
|
await InvalidatePermissionCacheAsync(actor, area, key);
|
||||||
cache.Remove(GetGroupsCacheKey(actor));
|
await cache.RemoveAsync(_GetGroupsCacheKey(actor));
|
||||||
|
await cache.RemoveGroupAsync(_GetPermissionGroupKey(actor));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InvalidatePermissionCache(string actor, string area, string key)
|
private async Task InvalidatePermissionCacheAsync(string actor, string area, string key)
|
||||||
{
|
{
|
||||||
var cacheKey = GetPermissionCacheKey(actor, area, key);
|
var cacheKey = _GetPermissionCacheKey(actor, area, key);
|
||||||
cache.Remove(cacheKey);
|
await cache.RemoveAsync(cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static T? _DeserializePermissionValue<T>(JsonDocument json)
|
private static T? _DeserializePermissionValue<T>(JsonDocument json)
|
||||||
|
@ -29,6 +29,7 @@ using Microsoft.OpenApi.Models;
|
|||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Serialization.SystemTextJson;
|
using NodaTime.Serialization.SystemTextJson;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
|
using StackExchange.Redis;
|
||||||
using tusdotnet;
|
using tusdotnet;
|
||||||
using tusdotnet.Models;
|
using tusdotnet.Models;
|
||||||
using tusdotnet.Models.Configuration;
|
using tusdotnet.Models.Configuration;
|
||||||
@ -44,6 +45,13 @@ builder.WebHost.ConfigureKestrel(options => options.Limits.MaxRequestBodySize =
|
|||||||
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
|
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
|
||||||
|
|
||||||
builder.Services.AddDbContext<AppDatabase>();
|
builder.Services.AddDbContext<AppDatabase>();
|
||||||
|
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
|
||||||
|
{
|
||||||
|
var connection = builder.Configuration.GetConnectionString("FastRetrieve")!;
|
||||||
|
return ConnectionMultiplexer.Connect(connection);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<ICacheService, CacheServiceRedis>();
|
||||||
|
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||||
|
@ -6,7 +6,7 @@ using NodaTime;
|
|||||||
|
|
||||||
namespace DysonNetwork.Sphere.Publisher;
|
namespace DysonNetwork.Sphere.Publisher;
|
||||||
|
|
||||||
public class PublisherService(AppDatabase db, FileService fs, IMemoryCache cache)
|
public class PublisherService(AppDatabase db, FileService fs, ICacheService cache)
|
||||||
{
|
{
|
||||||
public async Task<Publisher> CreateIndividualPublisher(
|
public async Task<Publisher> CreateIndividualPublisher(
|
||||||
Account.Account account,
|
Account.Account account,
|
||||||
@ -101,7 +101,8 @@ public class PublisherService(AppDatabase db, FileService fs, IMemoryCache cache
|
|||||||
public async Task<PublisherStats?> GetPublisherStats(string name)
|
public async Task<PublisherStats?> GetPublisherStats(string name)
|
||||||
{
|
{
|
||||||
var cacheKey = string.Format(PublisherStatsCacheKey, name);
|
var cacheKey = string.Format(PublisherStatsCacheKey, name);
|
||||||
if (cache.TryGetValue(cacheKey, out PublisherStats? stats))
|
var stats = await cache.GetAsync<PublisherStats>(cacheKey);
|
||||||
|
if (stats is not null)
|
||||||
return stats;
|
return stats;
|
||||||
|
|
||||||
var publisher = await db.Publishers.FirstOrDefaultAsync(e => e.Name == name);
|
var publisher = await db.Publishers.FirstOrDefaultAsync(e => e.Name == name);
|
||||||
@ -133,7 +134,7 @@ public class PublisherService(AppDatabase db, FileService fs, IMemoryCache cache
|
|||||||
SubscribersCount = subscribersCount,
|
SubscribersCount = subscribersCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
cache.Set(cacheKey, stats, TimeSpan.FromMinutes(5));
|
await cache.SetAsync(cacheKey, stats, TimeSpan.FromMinutes(5));
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,15 +158,16 @@ public class PublisherService(AppDatabase db, FileService fs, IMemoryCache cache
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
cache.Remove(string.Format(PublisherFeatureCacheKey, publisherId, flag));
|
await cache.RemoveAsync(string.Format(PublisherFeatureCacheKey, publisherId, flag));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> HasFeature(Guid publisherId, string flag)
|
public async Task<bool> HasFeature(Guid publisherId, string flag)
|
||||||
{
|
{
|
||||||
var cacheKey = string.Format(PublisherFeatureCacheKey, publisherId, flag);
|
var cacheKey = string.Format(PublisherFeatureCacheKey, publisherId, flag);
|
||||||
|
|
||||||
if (cache.TryGetValue(cacheKey, out bool isEnabled))
|
var isEnabled = await cache.GetAsync<bool?>(cacheKey);
|
||||||
return isEnabled;
|
if (isEnabled.HasValue)
|
||||||
|
return isEnabled.Value;
|
||||||
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
var featureFlag = await db.PublisherFeatures
|
var featureFlag = await db.PublisherFeatures
|
||||||
@ -175,17 +177,17 @@ public class PublisherService(AppDatabase db, FileService fs, IMemoryCache cache
|
|||||||
);
|
);
|
||||||
if (featureFlag is not null) isEnabled = true;
|
if (featureFlag is not null) isEnabled = true;
|
||||||
|
|
||||||
cache.Set(cacheKey, isEnabled, TimeSpan.FromMinutes(5));
|
await cache.SetAsync(cacheKey, isEnabled!.Value, TimeSpan.FromMinutes(5));
|
||||||
return isEnabled;
|
return isEnabled.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> IsMemberWithRole(Guid publisherId, Guid accountId, PublisherMemberRole requiredRole)
|
public async Task<bool> IsMemberWithRole(Guid publisherId, Guid accountId, PublisherMemberRole requiredRole)
|
||||||
{
|
{
|
||||||
var member = await db.Publishers
|
var member = await db.Publishers
|
||||||
.Where(p => p.Id == publisherId)
|
.Where(p => p.Id == publisherId)
|
||||||
.SelectMany(p => p.Members)
|
.SelectMany(p => p.Members)
|
||||||
.FirstOrDefaultAsync(m => m.AccountId == accountId);
|
.FirstOrDefaultAsync(m => m.AccountId == accountId);
|
||||||
|
|
||||||
return member != null && member.Role >= requiredRole;
|
return member != null && member.Role >= requiredRole;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,5 @@
|
|||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using System;
|
using System;
|
||||||
@ -8,19 +7,21 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Sticker;
|
namespace DysonNetwork.Sphere.Sticker;
|
||||||
public class StickerService(AppDatabase db, FileService fs, IMemoryCache cache)
|
|
||||||
|
public class StickerService(AppDatabase db, FileService fs, ICacheService cache)
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(15);
|
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(15);
|
||||||
|
|
||||||
public async Task<Sticker> CreateStickerAsync(Sticker sticker)
|
public async Task<Sticker> CreateStickerAsync(Sticker sticker)
|
||||||
{
|
{
|
||||||
db.Stickers.Add(sticker);
|
db.Stickers.Add(sticker);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await fs.MarkUsageAsync(sticker.Image, 1);
|
await fs.MarkUsageAsync(sticker.Image, 1);
|
||||||
|
|
||||||
return sticker;
|
return sticker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Sticker> UpdateStickerAsync(Sticker sticker, CloudFile? newImage)
|
public async Task<Sticker> UpdateStickerAsync(Sticker sticker, CloudFile? newImage)
|
||||||
{
|
{
|
||||||
if (newImage != null)
|
if (newImage != null)
|
||||||
@ -32,53 +33,54 @@ public class StickerService(AppDatabase db, FileService fs, IMemoryCache cache)
|
|||||||
|
|
||||||
db.Stickers.Update(sticker);
|
db.Stickers.Update(sticker);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// Invalidate cache for this sticker
|
// Invalidate cache for this sticker
|
||||||
PurgeStickerCache(sticker);
|
await PurgeStickerCache(sticker);
|
||||||
|
|
||||||
return sticker;
|
return sticker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteStickerAsync(Sticker sticker)
|
public async Task DeleteStickerAsync(Sticker sticker)
|
||||||
{
|
{
|
||||||
db.Stickers.Remove(sticker);
|
db.Stickers.Remove(sticker);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
await fs.MarkUsageAsync(sticker.Image, -1);
|
await fs.MarkUsageAsync(sticker.Image, -1);
|
||||||
|
|
||||||
// Invalidate cache for this sticker
|
// Invalidate cache for this sticker
|
||||||
PurgeStickerCache(sticker);
|
await PurgeStickerCache(sticker);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteStickerPackAsync(StickerPack pack)
|
public async Task DeleteStickerPackAsync(StickerPack pack)
|
||||||
{
|
{
|
||||||
var stickers = await db.Stickers
|
var stickers = await db.Stickers
|
||||||
.Include(s => s.Image)
|
.Include(s => s.Image)
|
||||||
.Where(s => s.PackId == pack.Id)
|
.Where(s => s.PackId == pack.Id)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var images = stickers.Select(s => s.Image).ToList();
|
var images = stickers.Select(s => s.Image).ToList();
|
||||||
|
|
||||||
db.Stickers.RemoveRange(stickers);
|
db.Stickers.RemoveRange(stickers);
|
||||||
db.StickerPacks.Remove(pack);
|
db.StickerPacks.Remove(pack);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await fs.MarkUsageRangeAsync(images, -1);
|
await fs.MarkUsageRangeAsync(images, -1);
|
||||||
|
|
||||||
// Invalidate cache for all stickers in this pack
|
// Invalidate cache for all stickers in this pack
|
||||||
foreach (var sticker in stickers)
|
foreach (var sticker in stickers)
|
||||||
{
|
{
|
||||||
PurgeStickerCache(sticker);
|
PurgeStickerCache(sticker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Sticker?> LookupStickerByIdentifierAsync(string identifier)
|
public async Task<Sticker?> LookupStickerByIdentifierAsync(string identifier)
|
||||||
{
|
{
|
||||||
identifier = identifier.ToLower();
|
identifier = identifier.ToLower();
|
||||||
// Try to get from the cache first
|
// Try to get from the cache first
|
||||||
var cacheKey = $"StickerLookup_{identifier}";
|
var cacheKey = $"StickerLookup_{identifier}";
|
||||||
if (cache.TryGetValue(cacheKey, out Sticker? cachedSticker))
|
var cachedSticker = await cache.GetAsync<Sticker>(cacheKey);
|
||||||
{
|
if (cachedSticker is not null)
|
||||||
return cachedSticker;
|
return cachedSticker;
|
||||||
}
|
|
||||||
|
|
||||||
// If not in cache, fetch from the database
|
// If not in cache, fetch from the database
|
||||||
IQueryable<Sticker> query = db.Stickers
|
IQueryable<Sticker> query = db.Stickers
|
||||||
.Include(e => e.Pack)
|
.Include(e => e.Pack)
|
||||||
@ -86,20 +88,20 @@ public class StickerService(AppDatabase db, FileService fs, IMemoryCache cache)
|
|||||||
query = Guid.TryParse(identifier, out var guid)
|
query = Guid.TryParse(identifier, out var guid)
|
||||||
? query.Where(e => e.Id == guid)
|
? query.Where(e => e.Id == guid)
|
||||||
: query.Where(e => (e.Pack.Prefix + e.Slug).ToLower() == identifier);
|
: query.Where(e => (e.Pack.Prefix + e.Slug).ToLower() == identifier);
|
||||||
|
|
||||||
var sticker = await query.FirstOrDefaultAsync();
|
var sticker = await query.FirstOrDefaultAsync();
|
||||||
|
|
||||||
// Store in cache if found
|
// Store in cache if found
|
||||||
if (sticker != null)
|
if (sticker != null)
|
||||||
cache.Set(cacheKey, sticker, CacheDuration);
|
await cache.SetAsync(cacheKey, sticker, CacheDuration);
|
||||||
|
|
||||||
return sticker;
|
return sticker;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PurgeStickerCache(Sticker sticker)
|
private async Task PurgeStickerCache(Sticker sticker)
|
||||||
{
|
{
|
||||||
// Remove both possible cache entries
|
// Remove both possible cache entries
|
||||||
cache.Remove($"StickerLookup_{sticker.Id}");
|
await cache.RemoveAsync($"StickerLookup_{sticker.Id}");
|
||||||
cache.Remove($"StickerLookup_{sticker.Pack.Prefix}{sticker.Slug}");
|
await cache.RemoveAsync($"StickerLookup_{sticker.Pack.Prefix}{sticker.Slug}");
|
||||||
}
|
}
|
||||||
}
|
}
|
356
DysonNetwork.Sphere/Storage/CacheService.cs
Normal file
356
DysonNetwork.Sphere/Storage/CacheService.cs
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Storage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a distributed lock that can be used to synchronize access across multiple processes
|
||||||
|
/// </summary>
|
||||||
|
public interface IDistributedLock : IAsyncDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The resource identifier this lock is protecting
|
||||||
|
/// </summary>
|
||||||
|
string Resource { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unique identifier for this lock instance
|
||||||
|
/// </summary>
|
||||||
|
string LockId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extends the lock's expiration time
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> ExtendAsync(TimeSpan timeSpan);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases the lock immediately
|
||||||
|
/// </summary>
|
||||||
|
Task ReleaseAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ICacheService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a value in the cache with an optional expiration time
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> SetAsync<T>(string key, T value, TimeSpan? expiry = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value from the cache
|
||||||
|
/// </summary>
|
||||||
|
Task<T?> GetAsync<T>(string key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a specific key from the cache
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> RemoveAsync(string key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a key to a group for group-based operations
|
||||||
|
/// </summary>
|
||||||
|
Task AddToGroupAsync(string key, string group);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all keys associated with a specific group
|
||||||
|
/// </summary>
|
||||||
|
Task RemoveGroupAsync(string group);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all keys belonging to a specific group
|
||||||
|
/// </summary>
|
||||||
|
Task<IEnumerable<string>> GetGroupKeysAsync(string group);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to set a value in cache and associate it with multiple groups in one operation
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of value being cached</typeparam>
|
||||||
|
/// <param name="key">Cache key</param>
|
||||||
|
/// <param name="value">The value to cache</param>
|
||||||
|
/// <param name="groups">Optional collection of group names to associate the key with</param>
|
||||||
|
/// <param name="expiry">Optional expiration time for the cached item</param>
|
||||||
|
/// <returns>True if the set operation was successful</returns>
|
||||||
|
Task<bool> SetWithGroupsAsync<T>(string key, T value, IEnumerable<string>? groups = null, TimeSpan? expiry = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acquires a distributed lock on the specified resource
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resource">The resource identifier to lock</param>
|
||||||
|
/// <param name="expiry">How long the lock should be held before automatically expiring</param>
|
||||||
|
/// <param name="waitTime">How long to wait for the lock before giving up</param>
|
||||||
|
/// <param name="retryInterval">How often to retry acquiring the lock during the wait time</param>
|
||||||
|
/// <returns>A distributed lock instance if acquired, null otherwise</returns>
|
||||||
|
Task<IDistributedLock?> AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes an action with a distributed lock, ensuring the lock is properly released afterwards
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resource">The resource identifier to lock</param>
|
||||||
|
/// <param name="action">The action to execute while holding the lock</param>
|
||||||
|
/// <param name="expiry">How long the lock should be held before automatically expiring</param>
|
||||||
|
/// <param name="waitTime">How long to wait for the lock before giving up</param>
|
||||||
|
/// <param name="retryInterval">How often to retry acquiring the lock during the wait time</param>
|
||||||
|
/// <returns>True if the lock was acquired and the action was executed, false otherwise</returns>
|
||||||
|
Task<bool> ExecuteWithLockAsync(string resource, Func<Task> action, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a function with a distributed lock, ensuring the lock is properly released afterwards
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The return type of the function</typeparam>
|
||||||
|
/// <param name="resource">The resource identifier to lock</param>
|
||||||
|
/// <param name="func">The function to execute while holding the lock</param>
|
||||||
|
/// <param name="expiry">How long the lock should be held before automatically expiring</param>
|
||||||
|
/// <param name="waitTime">How long to wait for the lock before giving up</param>
|
||||||
|
/// <param name="retryInterval">How often to retry acquiring the lock during the wait time</param>
|
||||||
|
/// <returns>The result of the function if the lock was acquired, default(T) otherwise</returns>
|
||||||
|
Task<(bool Acquired, T? Result)> ExecuteWithLockAsync<T>(string resource, Func<Task<T>> func, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RedisDistributedLock : IDistributedLock
|
||||||
|
{
|
||||||
|
private readonly IDatabase _database;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
private const string LockKeyPrefix = "Lock_";
|
||||||
|
|
||||||
|
public string Resource { get; }
|
||||||
|
public string LockId { get; }
|
||||||
|
|
||||||
|
internal RedisDistributedLock(IDatabase database, string resource, string lockId)
|
||||||
|
{
|
||||||
|
_database = database;
|
||||||
|
Resource = resource;
|
||||||
|
LockId = lockId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExtendAsync(TimeSpan timeSpan)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
throw new ObjectDisposedException(nameof(RedisDistributedLock));
|
||||||
|
|
||||||
|
var script = @"
|
||||||
|
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||||||
|
return redis.call('pexpire', KEYS[1], ARGV[2])
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
";
|
||||||
|
|
||||||
|
var result = await _database.ScriptEvaluateAsync(
|
||||||
|
script,
|
||||||
|
new RedisKey[] { $"{LockKeyPrefix}{Resource}" },
|
||||||
|
new RedisValue[] { LockId, (long)timeSpan.TotalMilliseconds }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (long)result! == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReleaseAsync()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var script = @"
|
||||||
|
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||||||
|
return redis.call('del', KEYS[1])
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
";
|
||||||
|
|
||||||
|
await _database.ScriptEvaluateAsync(
|
||||||
|
script,
|
||||||
|
new RedisKey[] { $"{LockKeyPrefix}{Resource}" },
|
||||||
|
new RedisValue[] { LockId }
|
||||||
|
);
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await ReleaseAsync();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CacheServiceRedis : ICacheService
|
||||||
|
{
|
||||||
|
private readonly IDatabase _database;
|
||||||
|
private readonly JsonSerializerOptions _serializerOptions;
|
||||||
|
|
||||||
|
// Using prefixes for different types of keys
|
||||||
|
private const string GroupKeyPrefix = "CacheGroup_";
|
||||||
|
private const string LockKeyPrefix = "Lock_";
|
||||||
|
|
||||||
|
public CacheServiceRedis(IConnectionMultiplexer redis)
|
||||||
|
{
|
||||||
|
var rds = redis ?? throw new ArgumentNullException(nameof(redis));
|
||||||
|
_database = rds.GetDatabase();
|
||||||
|
_serializerOptions = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetAsync<T>(string key, T value, TimeSpan? expiry = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentException("Key cannot be null or empty", nameof(key));
|
||||||
|
|
||||||
|
var serializedValue = JsonSerializer.Serialize(value, _serializerOptions);
|
||||||
|
return await _database.StringSetAsync(key, serializedValue, expiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T?> GetAsync<T>(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentException("Key cannot be null or empty", nameof(key));
|
||||||
|
|
||||||
|
var value = await _database.StringGetAsync(key);
|
||||||
|
|
||||||
|
if (value.IsNullOrEmpty)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<T>(value!, _serializerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RemoveAsync(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentException("Key cannot be null or empty", nameof(key));
|
||||||
|
|
||||||
|
// Before removing the key, find all groups it belongs to and remove it from them
|
||||||
|
var script = @"
|
||||||
|
local groups = redis.call('KEYS', ARGV[1])
|
||||||
|
for _, group in ipairs(groups) do
|
||||||
|
redis.call('SREM', group, ARGV[2])
|
||||||
|
end
|
||||||
|
return redis.call('DEL', ARGV[2])
|
||||||
|
";
|
||||||
|
|
||||||
|
var result = await _database.ScriptEvaluateAsync(
|
||||||
|
script,
|
||||||
|
values: new RedisValue[] { $"{GroupKeyPrefix}*", key }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (long)result! > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddToGroupAsync(string key, string group)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentException(@"Key cannot be null or empty.", nameof(key));
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(group))
|
||||||
|
throw new ArgumentException(@"Group cannot be null or empty.", nameof(group));
|
||||||
|
|
||||||
|
var groupKey = $"{GroupKeyPrefix}{group}";
|
||||||
|
await _database.SetAddAsync(groupKey, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveGroupAsync(string group)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(group))
|
||||||
|
throw new ArgumentException(@"Group cannot be null or empty.", nameof(group));
|
||||||
|
|
||||||
|
var groupKey = $"{GroupKeyPrefix}{group}";
|
||||||
|
|
||||||
|
// Get all keys in the group
|
||||||
|
var keys = await _database.SetMembersAsync(groupKey);
|
||||||
|
|
||||||
|
if (keys.Length > 0)
|
||||||
|
{
|
||||||
|
// Delete all the keys
|
||||||
|
var keysTasks = keys.Select(key => _database.KeyDeleteAsync(key.ToString()));
|
||||||
|
await Task.WhenAll(keysTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the group itself
|
||||||
|
await _database.KeyDeleteAsync(groupKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<string>> GetGroupKeysAsync(string group)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(group))
|
||||||
|
throw new ArgumentException(@"Group cannot be null or empty.", nameof(group));
|
||||||
|
|
||||||
|
var groupKey = $"{GroupKeyPrefix}{group}";
|
||||||
|
var members = await _database.SetMembersAsync(groupKey);
|
||||||
|
|
||||||
|
return members.Select(m => m.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetWithGroupsAsync<T>(string key, T value, IEnumerable<string>? groups = null, TimeSpan? expiry = null)
|
||||||
|
{
|
||||||
|
// First set the value in the cache
|
||||||
|
var setResult = await SetAsync(key, value, expiry);
|
||||||
|
|
||||||
|
// If successful and there are groups to associate, add the key to each group
|
||||||
|
if (setResult && groups != null)
|
||||||
|
{
|
||||||
|
var groupsArray = groups.Where(g => !string.IsNullOrEmpty(g)).ToArray();
|
||||||
|
if (groupsArray.Length > 0)
|
||||||
|
{
|
||||||
|
var tasks = groupsArray.Select(group => AddToGroupAsync(key, group));
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return setResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IDistributedLock?> AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(resource))
|
||||||
|
throw new ArgumentException("Resource cannot be null or empty", nameof(resource));
|
||||||
|
|
||||||
|
var lockKey = $"{LockKeyPrefix}{resource}";
|
||||||
|
var lockId = Guid.NewGuid().ToString("N");
|
||||||
|
var waitTimeSpan = waitTime ?? TimeSpan.Zero;
|
||||||
|
var retryIntervalSpan = retryInterval ?? TimeSpan.FromMilliseconds(100);
|
||||||
|
|
||||||
|
var startTime = DateTime.UtcNow;
|
||||||
|
var acquired = false;
|
||||||
|
|
||||||
|
// Try to acquire the lock, retry until waitTime is exceeded
|
||||||
|
while (!acquired && (DateTime.UtcNow - startTime) < waitTimeSpan)
|
||||||
|
{
|
||||||
|
acquired = await _database.StringSetAsync(lockKey, lockId, expiry, When.NotExists);
|
||||||
|
|
||||||
|
if (!acquired)
|
||||||
|
{
|
||||||
|
await Task.Delay(retryIntervalSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acquired)
|
||||||
|
{
|
||||||
|
return null; // Could not acquire the lock within the wait time
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RedisDistributedLock(_database, resource, lockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExecuteWithLockAsync(string resource, Func<Task> action, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
|
||||||
|
{
|
||||||
|
await using var lockObj = await AcquireLockAsync(resource, expiry, waitTime, retryInterval);
|
||||||
|
|
||||||
|
if (lockObj == null)
|
||||||
|
return false; // Could not acquire the lock
|
||||||
|
|
||||||
|
await action();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool Acquired, T? Result)> ExecuteWithLockAsync<T>(string resource, Func<Task<T>> func, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
|
||||||
|
{
|
||||||
|
await using var lockObj = await AcquireLockAsync(resource, expiry, waitTime, retryInterval);
|
||||||
|
|
||||||
|
if (lockObj == null)
|
||||||
|
return (false, default); // Could not acquire the lock
|
||||||
|
|
||||||
|
var result = await func();
|
||||||
|
return (true, result);
|
||||||
|
}
|
||||||
|
}
|
@ -17,12 +17,12 @@ public class FileService(
|
|||||||
TusDiskStore store,
|
TusDiskStore store,
|
||||||
ILogger<FileService> logger,
|
ILogger<FileService> logger,
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
IMemoryCache cache
|
ICacheService cache
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
private const string CacheKeyPrefix = "cloudfile_";
|
private const string CacheKeyPrefix = "cloudfile_";
|
||||||
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(15);
|
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(15);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The api for getting file meta with cache,
|
/// The api for getting file meta with cache,
|
||||||
/// the best use case is for accessing the file data.
|
/// the best use case is for accessing the file data.
|
||||||
@ -34,18 +34,19 @@ public class FileService(
|
|||||||
public async Task<CloudFile?> GetFileAsync(string fileId)
|
public async Task<CloudFile?> GetFileAsync(string fileId)
|
||||||
{
|
{
|
||||||
var cacheKey = $"{CacheKeyPrefix}{fileId}";
|
var cacheKey = $"{CacheKeyPrefix}{fileId}";
|
||||||
|
|
||||||
if (cache.TryGetValue(cacheKey, out CloudFile? cachedFile))
|
var cachedFile = await cache.GetAsync<CloudFile>(cacheKey);
|
||||||
|
if (cachedFile is not null)
|
||||||
return cachedFile;
|
return cachedFile;
|
||||||
|
|
||||||
var file = await db.Files.FirstOrDefaultAsync(f => f.Id == fileId);
|
var file = await db.Files.FirstOrDefaultAsync(f => f.Id == fileId);
|
||||||
|
|
||||||
if (file != null)
|
if (file != null)
|
||||||
cache.Set(cacheKey, file, CacheDuration);
|
await cache.SetAsync(cacheKey, file, CacheDuration);
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly string TempFilePrefix = "dyn-cloudfile";
|
private static readonly string TempFilePrefix = "dyn-cloudfile";
|
||||||
|
|
||||||
// The analysis file method no longer will remove the GPS EXIF data
|
// The analysis file method no longer will remove the GPS EXIF data
|
||||||
@ -83,7 +84,7 @@ public class FileService(
|
|||||||
file.FileMeta = existingFile.FileMeta;
|
file.FileMeta = existingFile.FileMeta;
|
||||||
file.HasCompression = existingFile.HasCompression;
|
file.HasCompression = existingFile.HasCompression;
|
||||||
file.SensitiveMarks = existingFile.SensitiveMarks;
|
file.SensitiveMarks = existingFile.SensitiveMarks;
|
||||||
|
|
||||||
db.Files.Add(file);
|
db.Files.Add(file);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return file;
|
return file;
|
||||||
@ -399,8 +400,7 @@ public class FileService(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public async Task SetExpiresRangeAsync(ICollection<CloudFile> files, Duration? duration)
|
public async Task SetExpiresRangeAsync(ICollection<CloudFile> files, Duration? duration)
|
||||||
{
|
{
|
||||||
@ -408,55 +408,55 @@ public class FileService(
|
|||||||
await db.Files.Where(o => ids.Contains(o.Id))
|
await db.Files.Where(o => ids.Contains(o.Id))
|
||||||
.ExecuteUpdateAsync(setter => setter.SetProperty(
|
.ExecuteUpdateAsync(setter => setter.SetProperty(
|
||||||
b => b.ExpiredAt,
|
b => b.ExpiredAt,
|
||||||
duration.HasValue
|
duration.HasValue
|
||||||
? b => SystemClock.Instance.GetCurrentInstant() + duration.Value
|
? b => SystemClock.Instance.GetCurrentInstant() + duration.Value
|
||||||
: _ => null
|
: _ => null
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(ICollection<CloudFile> current, ICollection<CloudFile> added, ICollection<CloudFile> removed)> DiffAndMarkFilesAsync(
|
public async Task<(ICollection<CloudFile> current, ICollection<CloudFile> added, ICollection<CloudFile> removed)>
|
||||||
ICollection<string>? newFileIds,
|
DiffAndMarkFilesAsync(
|
||||||
ICollection<CloudFile>? previousFiles = null
|
ICollection<string>? newFileIds,
|
||||||
)
|
ICollection<CloudFile>? previousFiles = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (newFileIds == null) return ([], [], previousFiles ?? []);
|
if (newFileIds == null) return ([], [], previousFiles ?? []);
|
||||||
|
|
||||||
var records = await db.Files.Where(f => newFileIds.Contains(f.Id)).ToListAsync();
|
var records = await db.Files.Where(f => newFileIds.Contains(f.Id)).ToListAsync();
|
||||||
var previous = previousFiles?.ToDictionary(f => f.Id) ?? new Dictionary<string, CloudFile>();
|
var previous = previousFiles?.ToDictionary(f => f.Id) ?? new Dictionary<string, CloudFile>();
|
||||||
var current = records.ToDictionary(f => f.Id);
|
var current = records.ToDictionary(f => f.Id);
|
||||||
|
|
||||||
var added = current.Keys.Except(previous.Keys).Select(id => current[id]).ToList();
|
var added = current.Keys.Except(previous.Keys).Select(id => current[id]).ToList();
|
||||||
var removed = previous.Keys.Except(current.Keys).Select(id => previous[id]).ToList();
|
var removed = previous.Keys.Except(current.Keys).Select(id => previous[id]).ToList();
|
||||||
|
|
||||||
if (added.Count > 0) await MarkUsageRangeAsync(added, 1);
|
if (added.Count > 0) await MarkUsageRangeAsync(added, 1);
|
||||||
if (removed.Count > 0) await MarkUsageRangeAsync(removed, -1);
|
if (removed.Count > 0) await MarkUsageRangeAsync(removed, -1);
|
||||||
|
|
||||||
return (newFileIds.Select(id => current[id]).ToList(), added, removed);
|
return (newFileIds.Select(id => current[id]).ToList(), added, removed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(ICollection<CloudFile> current, ICollection<CloudFile> added, ICollection<CloudFile> removed)> DiffAndSetExpiresAsync(
|
public async Task<(ICollection<CloudFile> current, ICollection<CloudFile> added, ICollection<CloudFile> removed)>
|
||||||
ICollection<string>? newFileIds,
|
DiffAndSetExpiresAsync(
|
||||||
Duration? duration,
|
ICollection<string>? newFileIds,
|
||||||
ICollection<CloudFile>? previousFiles = null
|
Duration? duration,
|
||||||
)
|
ICollection<CloudFile>? previousFiles = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (newFileIds == null) return ([], [], previousFiles ?? []);
|
if (newFileIds == null) return ([], [], previousFiles ?? []);
|
||||||
|
|
||||||
var records = await db.Files.Where(f => newFileIds.Contains(f.Id)).ToListAsync();
|
var records = await db.Files.Where(f => newFileIds.Contains(f.Id)).ToListAsync();
|
||||||
var previous = previousFiles?.ToDictionary(f => f.Id) ?? new Dictionary<string, CloudFile>();
|
var previous = previousFiles?.ToDictionary(f => f.Id) ?? new Dictionary<string, CloudFile>();
|
||||||
var current = records.ToDictionary(f => f.Id);
|
var current = records.ToDictionary(f => f.Id);
|
||||||
|
|
||||||
var added = current.Keys.Except(previous.Keys).Select(id => current[id]).ToList();
|
var added = current.Keys.Except(previous.Keys).Select(id => current[id]).ToList();
|
||||||
var removed = previous.Keys.Except(current.Keys).Select(id => previous[id]).ToList();
|
var removed = previous.Keys.Except(current.Keys).Select(id => previous[id]).ToList();
|
||||||
|
|
||||||
if (added.Count > 0) await SetExpiresRangeAsync(added, duration);
|
if (added.Count > 0) await SetExpiresRangeAsync(added, duration);
|
||||||
if (removed.Count > 0) await SetExpiresRangeAsync(removed, null);
|
if (removed.Count > 0) await SetExpiresRangeAsync(removed, null);
|
||||||
|
|
||||||
return (newFileIds.Select(id => current[id]).ToList(), added, removed);
|
return (newFileIds.Select(id => current[id]).ToList(), added, removed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CloudFileUnusedRecyclingJob(AppDatabase db, FileService fs, ILogger<CloudFileUnusedRecyclingJob> logger)
|
public class CloudFileUnusedRecyclingJob(AppDatabase db, FileService fs, ILogger<CloudFileUnusedRecyclingJob> logger)
|
||||||
|
@ -9,7 +9,8 @@
|
|||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"App": "Host=localhost;Port=5432;Database=dyson_network;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60"
|
"App": "Host=localhost;Port=5432;Database=dyson_network;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
|
||||||
|
"FastRetrieve": "localhost:6379"
|
||||||
},
|
},
|
||||||
"Authentication": {
|
"Authentication": {
|
||||||
"Schemes": {
|
"Schemes": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user