Merge remote-tracking branch 'origin/patch-notes' into patch-notes
This commit is contained in:
Submodule LightlessAPI updated: 44fbe10458...0bc7abb274
@@ -27,6 +27,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
private readonly Lock _fileWriteLock = new();
|
private readonly Lock _fileWriteLock = new();
|
||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly ILogger<FileCacheManager> _logger;
|
private readonly ILogger<FileCacheManager> _logger;
|
||||||
|
private bool _csvHeaderEnsured;
|
||||||
public string CacheFolder => _configService.Current.CacheFolder;
|
public string CacheFolder => _configService.Current.CacheFolder;
|
||||||
|
|
||||||
public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, LightlessConfigService configService, LightlessMediator lightlessMediator)
|
public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, LightlessConfigService configService, LightlessMediator lightlessMediator)
|
||||||
@@ -462,6 +463,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
string[] existingLines = File.ReadAllLines(_csvPath);
|
string[] existingLines = File.ReadAllLines(_csvPath);
|
||||||
if (existingLines.Length > 0 && TryParseVersionHeader(existingLines[0], out var existingVersion) && existingVersion == FileCacheVersion)
|
if (existingLines.Length > 0 && TryParseVersionHeader(existingLines[0], out var existingVersion) && existingVersion == FileCacheVersion)
|
||||||
{
|
{
|
||||||
|
_csvHeaderEnsured = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,6 +483,18 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
}
|
}
|
||||||
|
|
||||||
File.WriteAllText(_csvPath, rebuilt.ToString());
|
File.WriteAllText(_csvPath, rebuilt.ToString());
|
||||||
|
_csvHeaderEnsured = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureCsvHeaderLockedCached()
|
||||||
|
{
|
||||||
|
if (_csvHeaderEnsured)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureCsvHeaderLocked();
|
||||||
|
_csvHeaderEnsured = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BackupUnsupportedCache(string suffix)
|
private void BackupUnsupportedCache(string suffix)
|
||||||
@@ -540,10 +554,11 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
if (!File.Exists(_csvPath))
|
if (!File.Exists(_csvPath))
|
||||||
{
|
{
|
||||||
File.WriteAllLines(_csvPath, new[] { BuildVersionHeader(), entity.CsvEntry });
|
File.WriteAllLines(_csvPath, new[] { BuildVersionHeader(), entity.CsvEntry });
|
||||||
|
_csvHeaderEnsured = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
EnsureCsvHeaderLocked();
|
EnsureCsvHeaderLockedCached();
|
||||||
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,33 @@
|
|||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LightlessSync.FileCache;
|
namespace LightlessSync.FileCache;
|
||||||
|
|
||||||
public sealed class FileCompactor
|
public sealed class FileCompactor : IDisposable
|
||||||
{
|
{
|
||||||
public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U;
|
public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U;
|
||||||
public const ulong WOF_PROVIDER_FILE = 2UL;
|
public const ulong WOF_PROVIDER_FILE = 2UL;
|
||||||
|
|
||||||
private readonly Dictionary<string, int> _clusterSizes;
|
private readonly Dictionary<string, int> _clusterSizes;
|
||||||
|
private readonly ConcurrentDictionary<string, byte> _pendingCompactions;
|
||||||
private readonly WOF_FILE_COMPRESSION_INFO_V1 _efInfo;
|
private readonly WOF_FILE_COMPRESSION_INFO_V1 _efInfo;
|
||||||
private readonly ILogger<FileCompactor> _logger;
|
private readonly ILogger<FileCompactor> _logger;
|
||||||
|
|
||||||
private readonly LightlessConfigService _lightlessConfigService;
|
private readonly LightlessConfigService _lightlessConfigService;
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly Channel<string> _compactionQueue;
|
||||||
|
private readonly CancellationTokenSource _compactionCts = new();
|
||||||
|
private readonly Task _compactionWorker;
|
||||||
|
|
||||||
public FileCompactor(ILogger<FileCompactor> logger, LightlessConfigService lightlessConfigService, DalamudUtilService dalamudUtilService)
|
public FileCompactor(ILogger<FileCompactor> logger, LightlessConfigService lightlessConfigService, DalamudUtilService dalamudUtilService)
|
||||||
{
|
{
|
||||||
_clusterSizes = new(StringComparer.Ordinal);
|
_clusterSizes = new(StringComparer.Ordinal);
|
||||||
|
_pendingCompactions = new(StringComparer.OrdinalIgnoreCase);
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_lightlessConfigService = lightlessConfigService;
|
_lightlessConfigService = lightlessConfigService;
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
@@ -29,6 +37,18 @@ public sealed class FileCompactor
|
|||||||
Algorithm = CompressionAlgorithm.XPRESS8K,
|
Algorithm = CompressionAlgorithm.XPRESS8K,
|
||||||
Flags = 0
|
Flags = 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_compactionQueue = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
|
||||||
|
{
|
||||||
|
SingleReader = true,
|
||||||
|
SingleWriter = false
|
||||||
|
});
|
||||||
|
_compactionWorker = Task.Factory.StartNew(
|
||||||
|
() => ProcessQueueAsync(_compactionCts.Token),
|
||||||
|
_compactionCts.Token,
|
||||||
|
TaskCreationOptions.LongRunning,
|
||||||
|
TaskScheduler.Default)
|
||||||
|
.Unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum CompressionAlgorithm
|
private enum CompressionAlgorithm
|
||||||
@@ -87,7 +107,30 @@ public sealed class FileCompactor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CompactFile(filePath);
|
EnqueueCompaction(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_compactionQueue.Writer.TryComplete();
|
||||||
|
_compactionCts.Cancel();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_compactionWorker.Wait(TimeSpan.FromSeconds(5)))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Compaction worker did not shut down within timeout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Error shutting down compaction worker");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_compactionCts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("kernel32.dll")]
|
[DllImport("kernel32.dll")]
|
||||||
@@ -226,4 +269,67 @@ public sealed class FileCompactor
|
|||||||
public CompressionAlgorithm Algorithm;
|
public CompressionAlgorithm Algorithm;
|
||||||
public ulong Flags;
|
public ulong Flags;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private void EnqueueCompaction(string filePath)
|
||||||
|
{
|
||||||
|
if (!_pendingCompactions.TryAdd(filePath, 0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_compactionQueue.Writer.TryWrite(filePath))
|
||||||
|
{
|
||||||
|
_pendingCompactions.TryRemove(filePath, out _);
|
||||||
|
_logger.LogDebug("Failed to enqueue compaction job for {file}", filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessQueueAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (await _compactionQueue.Reader.WaitToReadAsync(token).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
while (_compactionQueue.Reader.TryRead(out var filePath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_dalamudUtilService.IsWine || !_lightlessConfigService.Current.UseCompactor)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Skipping compaction for missing file {file}", filePath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompactFile(filePath);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error compacting file {file}", filePath);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_pendingCompactions.TryRemove(filePath, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// expected during shutdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool ShowUploading { get; set; } = true;
|
public bool ShowUploading { get; set; } = true;
|
||||||
public bool ShowUploadingBigText { get; set; } = true;
|
public bool ShowUploadingBigText { get; set; } = true;
|
||||||
public bool ShowVisibleUsersSeparately { get; set; } = true;
|
public bool ShowVisibleUsersSeparately { get; set; } = true;
|
||||||
|
public bool EnableDirectDownloads { get; set; } = true;
|
||||||
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
||||||
public int TransferBarsHeight { get; set; } = 12;
|
public int TransferBarsHeight { get; set; } = 12;
|
||||||
public bool TransferBarsShowText { get; set; } = true;
|
public bool TransferBarsShowText { get; set; } = true;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.WebAPI.Files;
|
using LightlessSync.WebAPI.Files;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -10,21 +12,38 @@ public class FileDownloadManagerFactory
|
|||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
private readonly FileCompactor _fileCompactor;
|
private readonly FileCompactor _fileCompactor;
|
||||||
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
||||||
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
|
|
||||||
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, LightlessMediator lightlessMediator, FileTransferOrchestrator fileTransferOrchestrator,
|
public FileDownloadManagerFactory(
|
||||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor)
|
ILoggerFactory loggerFactory,
|
||||||
|
LightlessMediator lightlessMediator,
|
||||||
|
FileTransferOrchestrator fileTransferOrchestrator,
|
||||||
|
FileCacheManager fileCacheManager,
|
||||||
|
FileCompactor fileCompactor,
|
||||||
|
PairProcessingLimiter pairProcessingLimiter,
|
||||||
|
LightlessConfigService configService)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
_fileTransferOrchestrator = fileTransferOrchestrator;
|
_fileTransferOrchestrator = fileTransferOrchestrator;
|
||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
|
_configService = configService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileDownloadManager Create()
|
public FileDownloadManager Create()
|
||||||
{
|
{
|
||||||
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _lightlessMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor);
|
return new FileDownloadManager(
|
||||||
|
_loggerFactory.CreateLogger<FileDownloadManager>(),
|
||||||
|
_lightlessMediator,
|
||||||
|
_fileTransferOrchestrator,
|
||||||
|
_fileCacheManager,
|
||||||
|
_fileCompactor,
|
||||||
|
_pairProcessingLimiter,
|
||||||
|
_configService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (_allClientPairs.TryGetValue(user, out var pair))
|
if (_allClientPairs.TryGetValue(user, out var pair))
|
||||||
{
|
{
|
||||||
Mediator.Publish(new ClearProfileDataMessage(pair.UserData));
|
Mediator.Publish(new ClearProfileUserDataMessage(pair.UserData));
|
||||||
pair.MarkOffline();
|
pair.MarkOffline();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (!_allClientPairs.ContainsKey(dto.User)) throw new InvalidOperationException("No user found for " + dto);
|
if (!_allClientPairs.ContainsKey(dto.User)) throw new InvalidOperationException("No user found for " + dto);
|
||||||
|
|
||||||
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
Mediator.Publish(new ClearProfileUserDataMessage(dto.User));
|
||||||
|
|
||||||
var pair = _allClientPairs[dto.User];
|
var pair = _allClientPairs[dto.User];
|
||||||
if (pair.HasCachedPlayer)
|
if (pair.HasCachedPlayer)
|
||||||
@@ -254,7 +254,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (pair.UserPair.OtherPermissions.IsPaused() != dto.Permissions.IsPaused())
|
if (pair.UserPair.OtherPermissions.IsPaused() != dto.Permissions.IsPaused())
|
||||||
{
|
{
|
||||||
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
Mediator.Publish(new ClearProfileUserDataMessage(dto.User));
|
||||||
}
|
}
|
||||||
|
|
||||||
pair.UserPair.OtherPermissions = dto.Permissions;
|
pair.UserPair.OtherPermissions = dto.Permissions;
|
||||||
@@ -280,7 +280,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (pair.UserPair.OwnPermissions.IsPaused() != dto.Permissions.IsPaused())
|
if (pair.UserPair.OwnPermissions.IsPaused() != dto.Permissions.IsPaused())
|
||||||
{
|
{
|
||||||
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
Mediator.Publish(new ClearProfileUserDataMessage(dto.User));
|
||||||
}
|
}
|
||||||
|
|
||||||
pair.UserPair.OwnPermissions = dto.Permissions;
|
pair.UserPair.OwnPermissions = dto.Permissions;
|
||||||
|
|||||||
6
LightlessSync/Services/LightlessGroupProfileData.cs
Normal file
6
LightlessSync/Services/LightlessGroupProfileData.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
|
public record LightlessGroupProfileData(string Base64ProfilePicture, string Description, int[] Tags, bool IsNsfw, bool IsDisabled)
|
||||||
|
{
|
||||||
|
public Lazy<byte[]> ImageData { get; } = new Lazy<byte[]>(Convert.FromBase64String(Base64ProfilePicture));
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
public record LightlessProfileData(bool IsFlagged, bool IsNSFW, string Base64ProfilePicture, string Base64SupporterPicture, string Description)
|
public record LightlessUserProfileData(bool IsFlagged, bool IsNSFW, string Base64ProfilePicture, string Base64SupporterPicture, string Description)
|
||||||
{
|
{
|
||||||
public Lazy<byte[]> ImageData { get; } = new Lazy<byte[]>(Convert.FromBase64String(Base64ProfilePicture));
|
public Lazy<byte[]> ImageData { get; } = new Lazy<byte[]>(Convert.FromBase64String(Base64ProfilePicture));
|
||||||
public Lazy<byte[]> SupporterImageData { get; } = new Lazy<byte[]>(string.IsNullOrEmpty(Base64SupporterPicture) ? [] : Convert.FromBase64String(Base64SupporterPicture));
|
public Lazy<byte[]> SupporterImageData { get; } = new Lazy<byte[]>(string.IsNullOrEmpty(Base64SupporterPicture) ? [] : Convert.FromBase64String(Base64SupporterPicture));
|
||||||
@@ -70,7 +70,8 @@ public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<st
|
|||||||
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
|
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
|
||||||
public record UiToggleMessage(Type UiType) : MessageBase;
|
public record UiToggleMessage(Type UiType) : MessageBase;
|
||||||
public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase;
|
public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase;
|
||||||
public record ClearProfileDataMessage(UserData? UserData = null) : MessageBase;
|
public record ClearProfileUserDataMessage(UserData? UserData = null) : MessageBase;
|
||||||
|
public record ClearProfileGroupDataMessage(GroupData? GroupData = null) : MessageBase;
|
||||||
public record CyclePauseMessage(UserData UserData) : MessageBase;
|
public record CyclePauseMessage(UserData UserData) : MessageBase;
|
||||||
public record PauseMessage(UserData UserData) : MessageBase;
|
public record PauseMessage(UserData UserData) : MessageBase;
|
||||||
public record ProfilePopoutToggle(Pair? Pair) : MessageBase;
|
public record ProfilePopoutToggle(Pair? Pair) : MessageBase;
|
||||||
|
|||||||
@@ -208,7 +208,13 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
|
|
||||||
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
|
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
|
||||||
{
|
{
|
||||||
var objectInfo = ui3DModule->NamePlateObjectInfoPointers[i].Value;
|
if (ui3DModule->NamePlateObjectInfoPointers.IsEmpty) continue;
|
||||||
|
|
||||||
|
var objectInfoPtr = ui3DModule->NamePlateObjectInfoPointers[i];
|
||||||
|
|
||||||
|
if (objectInfoPtr == null) continue;
|
||||||
|
|
||||||
|
var objectInfo = objectInfoPtr.Value;
|
||||||
|
|
||||||
if (objectInfo == null || objectInfo->GameObject == null)
|
if (objectInfo == null || objectInfo->GameObject == null)
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
private readonly SemaphoreSlim _semaphore;
|
private readonly SemaphoreSlim _semaphore;
|
||||||
private int _currentLimit;
|
private int _currentLimit;
|
||||||
private int _pendingReductions;
|
private int _pendingReductions;
|
||||||
|
private int _pendingIncrements;
|
||||||
private int _waiting;
|
private int _waiting;
|
||||||
private int _inFlight;
|
private int _inFlight;
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!IsEnabled)
|
if (!IsEnabled)
|
||||||
{
|
{
|
||||||
_semaphore.Release();
|
TryReleaseSemaphore();
|
||||||
return NoopReleaser.Instance;
|
return NoopReleaser.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,18 +91,12 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
var releaseAmount = HardLimit - _semaphore.CurrentCount;
|
var releaseAmount = HardLimit - _semaphore.CurrentCount;
|
||||||
if (releaseAmount > 0)
|
if (releaseAmount > 0)
|
||||||
{
|
{
|
||||||
try
|
TryReleaseSemaphore(releaseAmount);
|
||||||
{
|
|
||||||
_semaphore.Release(releaseAmount);
|
|
||||||
}
|
|
||||||
catch (SemaphoreFullException)
|
|
||||||
{
|
|
||||||
// ignore, already at max
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentLimit = desiredLimit;
|
_currentLimit = desiredLimit;
|
||||||
_pendingReductions = 0;
|
_pendingReductions = 0;
|
||||||
|
_pendingIncrements = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,10 +108,13 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
if (desiredLimit > _currentLimit)
|
if (desiredLimit > _currentLimit)
|
||||||
{
|
{
|
||||||
var increment = desiredLimit - _currentLimit;
|
var increment = desiredLimit - _currentLimit;
|
||||||
var allowed = Math.Min(increment, HardLimit - _semaphore.CurrentCount);
|
_pendingIncrements += increment;
|
||||||
if (allowed > 0)
|
|
||||||
|
var available = HardLimit - _semaphore.CurrentCount;
|
||||||
|
var toRelease = Math.Min(_pendingIncrements, available);
|
||||||
|
if (toRelease > 0 && TryReleaseSemaphore(toRelease))
|
||||||
{
|
{
|
||||||
_semaphore.Release(allowed);
|
_pendingIncrements -= toRelease;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -133,6 +131,13 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_pendingReductions += remaining;
|
_pendingReductions += remaining;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_pendingIncrements > 0)
|
||||||
|
{
|
||||||
|
var offset = Math.Min(_pendingIncrements, _pendingReductions);
|
||||||
|
_pendingIncrements -= offset;
|
||||||
|
_pendingReductions -= offset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentLimit = desiredLimit;
|
_currentLimit = desiredLimit;
|
||||||
@@ -146,6 +151,25 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
return Math.Clamp(configured, 1, HardLimit);
|
return Math.Clamp(configured, 1, HardLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryReleaseSemaphore(int count = 1)
|
||||||
|
{
|
||||||
|
if (count <= 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_semaphore.Release(count);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (SemaphoreFullException ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(ex, "Attempted to release {count} pair processing slots but semaphore is already at the hard limit.", count);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ReleaseOne()
|
private void ReleaseOne()
|
||||||
{
|
{
|
||||||
var inFlight = Interlocked.Decrement(ref _inFlight);
|
var inFlight = Interlocked.Decrement(ref _inFlight);
|
||||||
@@ -166,9 +190,20 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
_pendingReductions--;
|
_pendingReductions--;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_pendingIncrements > 0)
|
||||||
|
{
|
||||||
|
if (!TryReleaseSemaphore())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingIncrements--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_semaphore.Release();
|
TryReleaseSemaphore();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using LightlessSync.API.Dto.Group;
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
|
using LightlessSync.API.Dto.Group;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
@@ -18,10 +19,11 @@ public class UiFactory
|
|||||||
private readonly ServerConfigurationManager _serverConfigManager;
|
private readonly ServerConfigurationManager _serverConfigManager;
|
||||||
private readonly LightlessProfileManager _lightlessProfileManager;
|
private readonly LightlessProfileManager _lightlessProfileManager;
|
||||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||||
|
private readonly FileDialogManager _fileDialogManager;
|
||||||
|
|
||||||
public UiFactory(ILoggerFactory loggerFactory, LightlessMediator lightlessMediator, ApiController apiController,
|
public UiFactory(ILoggerFactory loggerFactory, LightlessMediator lightlessMediator, ApiController apiController,
|
||||||
UiSharedService uiSharedService, PairManager pairManager, ServerConfigurationManager serverConfigManager,
|
UiSharedService uiSharedService, PairManager pairManager, ServerConfigurationManager serverConfigManager,
|
||||||
LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService)
|
LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService, FileDialogManager fileDialogManager)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
@@ -31,12 +33,13 @@ public class UiFactory
|
|||||||
_serverConfigManager = serverConfigManager;
|
_serverConfigManager = serverConfigManager;
|
||||||
_lightlessProfileManager = lightlessProfileManager;
|
_lightlessProfileManager = lightlessProfileManager;
|
||||||
_performanceCollectorService = performanceCollectorService;
|
_performanceCollectorService = performanceCollectorService;
|
||||||
|
_fileDialogManager = fileDialogManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
|
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
|
||||||
{
|
{
|
||||||
return new SyncshellAdminUI(_loggerFactory.CreateLogger<SyncshellAdminUI>(), _lightlessMediator,
|
return new SyncshellAdminUI(_loggerFactory.CreateLogger<SyncshellAdminUI>(), _lightlessMediator,
|
||||||
_apiController, _uiSharedService, _pairManager, dto, _performanceCollectorService);
|
_apiController, _uiSharedService, _pairManager, dto, _performanceCollectorService, _lightlessProfileManager, _fileDialogManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
Mediator.Subscribe<GposeStartMessage>(this, (_) => { _wasOpen = IsOpen; IsOpen = false; });
|
Mediator.Subscribe<GposeStartMessage>(this, (_) => { _wasOpen = IsOpen; IsOpen = false; });
|
||||||
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = _wasOpen);
|
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = _wasOpen);
|
||||||
Mediator.Subscribe<DisconnectedMessage>(this, (_) => IsOpen = false);
|
Mediator.Subscribe<DisconnectedMessage>(this, (_) => IsOpen = false);
|
||||||
Mediator.Subscribe<ClearProfileDataMessage>(this, (msg) =>
|
Mediator.Subscribe<ClearProfileUserDataMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
if (msg.UserData == null || string.Equals(msg.UserData.UID, _apiController.UID, StringComparison.Ordinal))
|
if (msg.UserData == null || string.Equals(msg.UserData.UID, _apiController.UID, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -91,6 +91,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
{
|
{
|
||||||
|
|
||||||
_uiSharedService.UnderlinedBigText("Notes and Rules for Profiles", UIColors.Get("LightlessYellow"));
|
_uiSharedService.UnderlinedBigText("Notes and Rules for Profiles", UIColors.Get("LightlessYellow"));
|
||||||
ImGui.Dummy(new Vector2(5));
|
ImGui.Dummy(new Vector2(5));
|
||||||
|
|
||||||
@@ -108,7 +109,8 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
ImGui.Dummy(new Vector2(3));
|
ImGui.Dummy(new Vector2(3));
|
||||||
|
|
||||||
var profile = _lightlessProfileManager.GetLightlessProfile(new UserData(_apiController.UID));
|
var profile = _lightlessProfileManager.GetLightlessUserProfile(new UserData(_apiController.UID));
|
||||||
|
_logger.LogInformation("Profile fetched for drawing: {profile}", profile);
|
||||||
|
|
||||||
if (ImGui.BeginTabBar("##EditProfileTabs"))
|
if (ImGui.BeginTabBar("##EditProfileTabs"))
|
||||||
{
|
{
|
||||||
@@ -204,7 +206,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
_showFileDialogError = false;
|
_showFileDialogError = false;
|
||||||
await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null))
|
await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null, Tags: null))
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -213,7 +215,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture"))
|
||||||
{
|
{
|
||||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null));
|
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null, Tags: null));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
|
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
|
||||||
if (_showFileDialogError)
|
if (_showFileDialogError)
|
||||||
@@ -223,7 +225,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
var isNsfw = profile.IsNSFW;
|
var isNsfw = profile.IsNSFW;
|
||||||
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
|
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
|
||||||
{
|
{
|
||||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null));
|
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null, Tags: null));
|
||||||
}
|
}
|
||||||
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
|
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
|
||||||
var widthTextBox = 400;
|
var widthTextBox = 400;
|
||||||
@@ -262,13 +264,13 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
|
||||||
{
|
{
|
||||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText));
|
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText, Tags: null));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Sets your profile description text");
|
UiSharedService.AttachToolTip("Sets your profile description text");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
|
||||||
{
|
{
|
||||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, ""));
|
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, "", Tags: null));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Clears your profile description text");
|
UiSharedService.AttachToolTip("Clears your profile description text");
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing;
|
var spacing = ImGui.GetStyle().ItemSpacing;
|
||||||
|
|
||||||
var lightlessProfile = _lightlessProfileManager.GetLightlessProfile(_pair.UserData);
|
var lightlessProfile = _lightlessProfileManager.GetLightlessUserProfile(_pair.UserData);
|
||||||
|
|
||||||
if (_textureWrap == null || !lightlessProfile.ImageData.Value.SequenceEqual(_lastProfilePicture))
|
if (_textureWrap == null || !lightlessProfile.ImageData.Value.SequenceEqual(_lastProfilePicture))
|
||||||
{
|
{
|
||||||
|
|||||||
12
LightlessSync/UI/ProfileTags.cs
Normal file
12
LightlessSync/UI/ProfileTags.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace LightlessSync.UI
|
||||||
|
{
|
||||||
|
public enum ProfileTags
|
||||||
|
{
|
||||||
|
SFW = 0,
|
||||||
|
NSFW = 1,
|
||||||
|
RP = 2,
|
||||||
|
ERP = 3,
|
||||||
|
Venues = 4,
|
||||||
|
Gpose = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -610,6 +610,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
bool limitPairApplications = _configService.Current.EnablePairProcessingLimiter;
|
bool limitPairApplications = _configService.Current.EnablePairProcessingLimiter;
|
||||||
bool useAlternativeUpload = _configService.Current.UseAlternativeFileUpload;
|
bool useAlternativeUpload = _configService.Current.UseAlternativeFileUpload;
|
||||||
int downloadSpeedLimit = _configService.Current.DownloadSpeedLimitInBytes;
|
int downloadSpeedLimit = _configService.Current.DownloadSpeedLimitInBytes;
|
||||||
|
bool enableDirectDownloads = _configService.Current.EnableDirectDownloads;
|
||||||
|
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
ImGui.TextUnformatted("Global Download Speed Limit");
|
ImGui.TextUnformatted("Global Download Speed Limit");
|
||||||
@@ -641,6 +642,13 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
ImGui.TextUnformatted("0 = No limit/infinite");
|
ImGui.TextUnformatted("0 = No limit/infinite");
|
||||||
|
|
||||||
|
if (ImGui.Checkbox("[BETA] Enable Lightspeed Downloads", ref enableDirectDownloads))
|
||||||
|
{
|
||||||
|
_configService.Current.EnableDirectDownloads = enableDirectDownloads;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("Uses signed CDN links when available. Disable to force the legacy queued download flow.");
|
||||||
|
|
||||||
if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10))
|
if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10))
|
||||||
{
|
{
|
||||||
_configService.Current.ParallelDownloads = maxParallelDownloads;
|
_configService.Current.ParallelDownloads = maxParallelDownloads;
|
||||||
@@ -2313,7 +2321,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (ImGui.Checkbox("Show Lightless Profiles on Hover", ref showProfiles))
|
if (ImGui.Checkbox("Show Lightless Profiles on Hover", ref showProfiles))
|
||||||
{
|
{
|
||||||
Mediator.Publish(new ClearProfileDataMessage());
|
Mediator.Publish(new ClearProfileUserDataMessage());
|
||||||
_configService.Current.ProfilesShow = showProfiles;
|
_configService.Current.ProfilesShow = showProfiles;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
}
|
}
|
||||||
@@ -2340,7 +2348,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.Unindent();
|
ImGui.Unindent();
|
||||||
if (ImGui.Checkbox("Show profiles marked as NSFW", ref showNsfwProfiles))
|
if (ImGui.Checkbox("Show profiles marked as NSFW", ref showNsfwProfiles))
|
||||||
{
|
{
|
||||||
Mediator.Publish(new ClearProfileDataMessage());
|
Mediator.Publish(new ClearProfileUserDataMessage());
|
||||||
_configService.Current.ProfilesAllowNsfw = showNsfwProfiles;
|
_configService.Current.ProfilesAllowNsfw = showNsfwProfiles;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing;
|
var spacing = ImGui.GetStyle().ItemSpacing;
|
||||||
|
|
||||||
var lightlessProfile = _lightlessProfileManager.GetLightlessProfile(Pair.UserData);
|
var lightlessProfile = _lightlessProfileManager.GetLightlessUserProfile(Pair.UserData);
|
||||||
|
|
||||||
if (_textureWrap == null || !lightlessProfile.ImageData.Value.SequenceEqual(_lastProfilePicture))
|
if (_textureWrap == null || !lightlessProfile.ImageData.Value.SequenceEqual(_lastProfilePicture))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
|
using Dalamud.Interface.Textures.TextureWraps;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Data.Extensions;
|
using LightlessSync.API.Data.Extensions;
|
||||||
using LightlessSync.API.Dto.Group;
|
using LightlessSync.API.Dto.Group;
|
||||||
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.UI.Handlers;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace LightlessSync.UI;
|
namespace LightlessSync.UI;
|
||||||
|
|
||||||
@@ -22,29 +31,51 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
private readonly bool _isOwner = false;
|
private readonly bool _isOwner = false;
|
||||||
private readonly List<string> _oneTimeInvites = [];
|
private readonly List<string> _oneTimeInvites = [];
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly LightlessProfileManager _lightlessProfileManager;
|
||||||
|
private readonly FileDialogManager _fileDialogManager;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private List<BannedGroupUserDto> _bannedUsers = [];
|
private List<BannedGroupUserDto> _bannedUsers = [];
|
||||||
|
private LightlessGroupProfileData? _profileData = null;
|
||||||
|
private bool _adjustedForScollBarsLocalProfile = false;
|
||||||
|
private bool _adjustedForScollBarsOnlineProfile = false;
|
||||||
|
private string _descriptionText = string.Empty;
|
||||||
|
private IDalamudTextureWrap? _pfpTextureWrap;
|
||||||
|
private string _profileDescription = string.Empty;
|
||||||
|
private byte[] _profileImage = [];
|
||||||
|
private bool _showFileDialogError = false;
|
||||||
private int _multiInvites;
|
private int _multiInvites;
|
||||||
private string _newPassword;
|
private string _newPassword;
|
||||||
private bool _pwChangeSuccess;
|
private bool _pwChangeSuccess;
|
||||||
private Task<int>? _pruneTestTask;
|
private Task<int>? _pruneTestTask;
|
||||||
private Task<int>? _pruneTask;
|
private Task<int>? _pruneTask;
|
||||||
private int _pruneDays = 14;
|
private int _pruneDays = 14;
|
||||||
|
private List<int> _selectedTags = [];
|
||||||
|
|
||||||
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, LightlessMediator mediator, ApiController apiController,
|
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, LightlessMediator mediator, ApiController apiController,
|
||||||
UiSharedService uiSharedService, PairManager pairManager, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService)
|
UiSharedService uiSharedService, PairManager pairManager, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager, FileDialogManager fileDialogManager)
|
||||||
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
|
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
|
||||||
{
|
{
|
||||||
GroupFullInfo = groupFullInfo;
|
GroupFullInfo = groupFullInfo;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
|
_lightlessProfileManager = lightlessProfileManager;
|
||||||
|
_fileDialogManager = fileDialogManager;
|
||||||
|
|
||||||
_isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal);
|
_isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal);
|
||||||
_isModerator = GroupFullInfo.GroupUserInfo.IsModerator();
|
_isModerator = GroupFullInfo.GroupUserInfo.IsModerator();
|
||||||
_newPassword = string.Empty;
|
_newPassword = string.Empty;
|
||||||
_multiInvites = 30;
|
_multiInvites = 30;
|
||||||
_pwChangeSuccess = true;
|
_pwChangeSuccess = true;
|
||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
|
Mediator.Subscribe<ClearProfileGroupDataMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (msg.GroupData == null || string.Equals(msg.GroupData.AliasOrGID, GroupFullInfo.Group.AliasOrGID, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_pfpTextureWrap?.Dispose();
|
||||||
|
_pfpTextureWrap = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
SizeConstraints = new WindowSizeConstraints()
|
SizeConstraints = new WindowSizeConstraints()
|
||||||
{
|
{
|
||||||
MinimumSize = new(700, 500),
|
MinimumSize = new(700, 500),
|
||||||
@@ -58,10 +89,13 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (!_isModerator && !_isOwner) return;
|
if (!_isModerator && !_isOwner) return;
|
||||||
|
|
||||||
|
_logger.LogTrace("Drawing Syncshell Admin UI for {group}", GroupFullInfo.GroupAliasOrGID);
|
||||||
GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group];
|
GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group];
|
||||||
|
|
||||||
using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID);
|
_profileData = _lightlessProfileManager.GetLightlessGroupProfile(GroupFullInfo.Group);
|
||||||
|
GetTagsFromProfile();
|
||||||
|
|
||||||
|
using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID);
|
||||||
using (_uiSharedService.UidFont.Push())
|
using (_uiSharedService.UidFont.Push())
|
||||||
_uiSharedService.UnderlinedBigText(GroupFullInfo.GroupAliasOrGID + " Administrative Panel", UIColors.Get("LightlessBlue"));
|
_uiSharedService.UnderlinedBigText(GroupFullInfo.GroupAliasOrGID + " Administrative Panel", UIColors.Get("LightlessBlue"));
|
||||||
|
|
||||||
@@ -69,14 +103,16 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
var perm = GroupFullInfo.GroupPermissions;
|
var perm = GroupFullInfo.GroupPermissions;
|
||||||
|
|
||||||
using var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID);
|
using var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID);
|
||||||
|
|
||||||
if (tabbar)
|
if (tabbar)
|
||||||
{
|
{
|
||||||
DrawInvites(perm);
|
DrawInvites(perm);
|
||||||
|
|
||||||
DrawManagement();
|
DrawManagement();
|
||||||
|
|
||||||
DrawPermission(perm);
|
DrawPermission(perm);
|
||||||
|
|
||||||
|
DrawProfile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +212,184 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
ownerTab.Dispose();
|
ownerTab.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private void DrawProfile()
|
||||||
|
{
|
||||||
|
var profileTab = ImRaii.TabItem("Profile");
|
||||||
|
|
||||||
|
if (profileTab)
|
||||||
|
{
|
||||||
|
if (_uiSharedService.MediumTreeNode("Current Profile", UIColors.Get("LightlessPurple")))
|
||||||
|
{
|
||||||
|
ImGui.Dummy(new Vector2(5));
|
||||||
|
|
||||||
|
if (!_profileImage.SequenceEqual(_profileData.ImageData.Value))
|
||||||
|
{
|
||||||
|
_profileImage = _profileData.ImageData.Value;
|
||||||
|
_pfpTextureWrap?.Dispose();
|
||||||
|
_pfpTextureWrap = _uiSharedService.LoadImage(_profileImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(_profileDescription, _profileData.Description, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_profileDescription = _profileData.Description;
|
||||||
|
_descriptionText = _profileDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pfpTextureWrap != null)
|
||||||
|
{
|
||||||
|
ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height));
|
||||||
|
}
|
||||||
|
|
||||||
|
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||||
|
ImGuiHelpers.ScaledRelativeSameLine(256, spacing);
|
||||||
|
using (_uiSharedService.GameFont.Push())
|
||||||
|
{
|
||||||
|
var descriptionTextSize = ImGui.CalcTextSize(_profileData.Description, wrapWidth: 256f);
|
||||||
|
var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256);
|
||||||
|
if (descriptionTextSize.Y > childFrame.Y)
|
||||||
|
{
|
||||||
|
_adjustedForScollBarsOnlineProfile = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_adjustedForScollBarsOnlineProfile = false;
|
||||||
|
}
|
||||||
|
childFrame = childFrame with
|
||||||
|
{
|
||||||
|
X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0),
|
||||||
|
};
|
||||||
|
if (ImGui.BeginChildFrame(101, childFrame))
|
||||||
|
{
|
||||||
|
UiSharedService.TextWrapped(_profileData.Description);
|
||||||
|
}
|
||||||
|
ImGui.EndChildFrame();
|
||||||
|
ImGui.TreePop();
|
||||||
|
}
|
||||||
|
var nsfw = _profileData.IsNsfw;
|
||||||
|
ImGui.BeginDisabled();
|
||||||
|
ImGui.Checkbox("Is NSFW", ref nsfw);
|
||||||
|
ImGui.EndDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
|
||||||
|
if (_uiSharedService.MediumTreeNode("Profile Settings", UIColors.Get("LightlessPurple")))
|
||||||
|
{
|
||||||
|
ImGui.Dummy(new Vector2(5));
|
||||||
|
ImGui.TextUnformatted($"Profile Picture:");
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
|
||||||
|
{
|
||||||
|
_fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) =>
|
||||||
|
{
|
||||||
|
if (!success) return;
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var fileContent = await File.ReadAllBytesAsync(file).ConfigureAwait(false);
|
||||||
|
MemoryStream ms = new(fileContent);
|
||||||
|
await using (ms.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
|
||||||
|
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_showFileDialogError = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
using var image = Image.Load<Rgba32>(fileContent);
|
||||||
|
|
||||||
|
if (image.Width > 512 || image.Height > 512 || (fileContent.Length > 2000 * 1024))
|
||||||
|
{
|
||||||
|
_showFileDialogError = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_showFileDialogError = false;
|
||||||
|
await _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, Convert.ToBase64String(fileContent), IsNsfw: null, IsDisabled: null))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Select and upload a new profile picture");
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture"))
|
||||||
|
{
|
||||||
|
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, IsNsfw: null, IsDisabled: null));
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
|
||||||
|
if (_showFileDialogError)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed);
|
||||||
|
}
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.TextUnformatted($"Tags:");
|
||||||
|
var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200);
|
||||||
|
|
||||||
|
var allCategoryIndexes = Enum.GetValues<ProfileTags>()
|
||||||
|
.Cast<int>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach(int tag in allCategoryIndexes)
|
||||||
|
{
|
||||||
|
using (ImRaii.PushId($"tag-{tag}")) DrawTag(tag);
|
||||||
|
}
|
||||||
|
ImGui.Separator();
|
||||||
|
var widthTextBox = 400;
|
||||||
|
var posX = ImGui.GetCursorPosX();
|
||||||
|
ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500");
|
||||||
|
ImGui.SetCursorPosX(posX);
|
||||||
|
ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X);
|
||||||
|
ImGui.TextUnformatted("Preview (approximate)");
|
||||||
|
using (_uiSharedService.GameFont.Push())
|
||||||
|
ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200));
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
using (_uiSharedService.GameFont.Push())
|
||||||
|
{
|
||||||
|
var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f);
|
||||||
|
if (descriptionTextSizeLocal.Y > childFrameLocal.Y)
|
||||||
|
{
|
||||||
|
_adjustedForScollBarsLocalProfile = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_adjustedForScollBarsLocalProfile = false;
|
||||||
|
}
|
||||||
|
childFrameLocal = childFrameLocal with
|
||||||
|
{
|
||||||
|
X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0),
|
||||||
|
};
|
||||||
|
if (ImGui.BeginChildFrame(102, childFrameLocal))
|
||||||
|
{
|
||||||
|
UiSharedService.TextWrapped(_descriptionText);
|
||||||
|
}
|
||||||
|
ImGui.EndChildFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
|
||||||
|
{
|
||||||
|
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: _descriptionText, Tags: null, PictureBase64: null, IsNsfw: null, IsDisabled: null));
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Sets your profile description text");
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
|
||||||
|
{
|
||||||
|
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, IsNsfw: null, IsDisabled: null));
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Clears your profile description text");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.TextUnformatted($"Profile Options:");
|
||||||
|
var isNsfw = _profileData.IsNsfw;
|
||||||
|
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
|
||||||
|
{
|
||||||
|
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, IsNsfw: isNsfw, IsDisabled: null));
|
||||||
|
}
|
||||||
|
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
|
||||||
|
ImGui.TreePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profileTab.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawManagement()
|
private void DrawManagement()
|
||||||
{
|
{
|
||||||
@@ -192,7 +406,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
var tableFlags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp;
|
var tableFlags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp;
|
||||||
if (pairs.Count > 10) tableFlags |= ImGuiTableFlags.ScrollY;
|
if (pairs.Count > 10) tableFlags |= ImGuiTableFlags.ScrollY;
|
||||||
using var table = ImRaii.Table("userList#" + GroupFullInfo.Group.GID, 3, tableFlags);
|
using var table = ImRaii.Table("userList#" + GroupFullInfo.Group.AliasOrGID, 3, tableFlags);
|
||||||
if (table)
|
if (table)
|
||||||
{
|
{
|
||||||
ImGui.TableSetupColumn("Alias/UID/Note", ImGuiTableColumnFlags.None, 4);
|
ImGui.TableSetupColumn("Alias/UID/Note", ImGuiTableColumnFlags.None, 4);
|
||||||
@@ -474,7 +688,6 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
}
|
}
|
||||||
mgmtTab.Dispose();
|
mgmtTab.Dispose();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawInvites(GroupPermissions perm)
|
private void DrawInvites(GroupPermissions perm)
|
||||||
@@ -521,9 +734,37 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
inviteTab.Dispose();
|
inviteTab.Dispose();
|
||||||
}
|
}
|
||||||
|
private void DrawTag(int tag)
|
||||||
|
{
|
||||||
|
var HasTag = _selectedTags.Contains(tag);
|
||||||
|
var tagName = (ProfileTags)tag;
|
||||||
|
|
||||||
|
if (ImGui.Checkbox(tagName.ToString(), ref HasTag))
|
||||||
|
{
|
||||||
|
if (HasTag)
|
||||||
|
{
|
||||||
|
_selectedTags.Add(tag);
|
||||||
|
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: _selectedTags.ToArray(), PictureBase64: null, IsNsfw: null, IsDisabled: null));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selectedTags.Remove(tag);
|
||||||
|
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: _selectedTags.ToArray(), PictureBase64: null, IsNsfw: null, IsDisabled: null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GetTagsFromProfile()
|
||||||
|
{
|
||||||
|
if (_profileData != null)
|
||||||
|
{
|
||||||
|
_selectedTags = [.. _profileData.Tags];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
{
|
{
|
||||||
Mediator.Publish(new RemoveWindowMessage(this));
|
Mediator.Publish(new RemoveWindowMessage(this));
|
||||||
|
_pfpTextureWrap?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,8 +288,6 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentGids = _nearbySyncshells.Select(s => s.Group.GID).ToHashSet(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
if (updatedList != null)
|
if (updatedList != null)
|
||||||
{
|
{
|
||||||
var previousGid = GetSelectedGid();
|
var previousGid = GetSelectedGid();
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Security.Cryptography;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace LightlessSync.Utils;
|
namespace LightlessSync.Utils;
|
||||||
@@ -13,8 +16,9 @@ public static class Crypto
|
|||||||
|
|
||||||
public static string GetFileHash(this string filePath)
|
public static string GetFileHash(this string filePath)
|
||||||
{
|
{
|
||||||
using SHA1CryptoServiceProvider cryptoProvider = new();
|
using SHA1 sha1 = SHA1.Create();
|
||||||
return BitConverter.ToString(cryptoProvider.ComputeHash(File.ReadAllBytes(filePath))).Replace("-", "", StringComparison.Ordinal);
|
using FileStream stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
||||||
|
return BitConverter.ToString(sha1.ComputeHash(stream)).Replace("-", "", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetHash256(this (string, ushort) playerToHash)
|
public static string GetHash256(this (string, ushort) playerToHash)
|
||||||
|
|||||||
@@ -5,12 +5,18 @@ using LightlessSync.API.Dto.Files;
|
|||||||
using LightlessSync.API.Routes;
|
using LightlessSync.API.Routes;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.WebAPI.Files.Models;
|
using LightlessSync.WebAPI.Files.Models;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
|
||||||
namespace LightlessSync.WebAPI.Files;
|
namespace LightlessSync.WebAPI.Files;
|
||||||
|
|
||||||
@@ -20,17 +26,27 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
private readonly FileCompactor _fileCompactor;
|
private readonly FileCompactor _fileCompactor;
|
||||||
private readonly FileCacheManager _fileDbManager;
|
private readonly FileCacheManager _fileDbManager;
|
||||||
private readonly FileTransferOrchestrator _orchestrator;
|
private readonly FileTransferOrchestrator _orchestrator;
|
||||||
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams;
|
private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams;
|
||||||
|
private static readonly TimeSpan DownloadStallTimeout = TimeSpan.FromSeconds(30);
|
||||||
|
private volatile bool _disableDirectDownloads;
|
||||||
|
private int _consecutiveDirectDownloadFailures;
|
||||||
|
private bool _lastConfigDirectDownloadsState;
|
||||||
|
|
||||||
public FileDownloadManager(ILogger<FileDownloadManager> logger, LightlessMediator mediator,
|
public FileDownloadManager(ILogger<FileDownloadManager> logger, LightlessMediator mediator,
|
||||||
FileTransferOrchestrator orchestrator,
|
FileTransferOrchestrator orchestrator,
|
||||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor) : base(logger, mediator)
|
FileCacheManager fileCacheManager, FileCompactor fileCompactor,
|
||||||
|
PairProcessingLimiter pairProcessingLimiter, LightlessConfigService configService) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
|
_downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
|
||||||
_orchestrator = orchestrator;
|
_orchestrator = orchestrator;
|
||||||
_fileDbManager = fileCacheManager;
|
_fileDbManager = fileCacheManager;
|
||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
|
_configService = configService;
|
||||||
_activeDownloadStreams = new();
|
_activeDownloadStreams = new();
|
||||||
|
_lastConfigDirectDownloadsState = _configService.Current.EnableDirectDownloads;
|
||||||
|
|
||||||
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
|
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
@@ -50,6 +66,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public bool IsDownloading => CurrentDownloads.Any();
|
public bool IsDownloading => CurrentDownloads.Any();
|
||||||
|
|
||||||
|
private bool ShouldUseDirectDownloads()
|
||||||
|
{
|
||||||
|
return _configService.Current.EnableDirectDownloads && !_disableDirectDownloads;
|
||||||
|
}
|
||||||
|
|
||||||
public static void MungeBuffer(Span<byte> buffer)
|
public static void MungeBuffer(Span<byte> buffer)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < buffer.Length; ++i)
|
for (int i = 0; i < buffer.Length; ++i)
|
||||||
@@ -156,39 +177,47 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
Logger.LogWarning("Download status missing for {group} when starting download", downloadGroup);
|
Logger.LogWarning("Download status missing for {group} when starting download", downloadGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var requestUrl = LightlessFiles.CacheGetFullPath(fileTransfer[0].DownloadUri, requestId);
|
||||||
|
|
||||||
|
await DownloadFileThrottled(requestUrl, tempPath, progress, MungeBuffer, ct, withToken: true).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private delegate void DownloadDataCallback(Span<byte> data);
|
||||||
|
|
||||||
|
private async Task DownloadFileThrottled(Uri requestUrl, string destinationFilename, IProgress<long> progress, DownloadDataCallback? callback, CancellationToken ct, bool withToken)
|
||||||
|
{
|
||||||
const int maxRetries = 3;
|
const int maxRetries = 3;
|
||||||
int retryCount = 0;
|
int retryCount = 0;
|
||||||
TimeSpan retryDelay = TimeSpan.FromSeconds(2);
|
TimeSpan retryDelay = TimeSpan.FromSeconds(2);
|
||||||
|
HttpResponseMessage? response = null;
|
||||||
HttpResponseMessage response = null!;
|
|
||||||
var requestUrl = LightlessFiles.CacheGetFullPath(fileTransfer[0].DownloadUri, requestId);
|
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Attempt {attempt} - Downloading {requestUrl} for request {id}", retryCount + 1, requestUrl, requestId);
|
Logger.LogDebug("Attempt {attempt} - Downloading {requestUrl}", retryCount + 1, requestUrl);
|
||||||
|
response = await _orchestrator.SendRequestAsync(HttpMethod.Get, requestUrl, ct, HttpCompletionOption.ResponseHeadersRead, withToken).ConfigureAwait(false);
|
||||||
response = await _orchestrator.SendRequestAsync(HttpMethod.Get, requestUrl, ct, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex) when (ex.InnerException is TimeoutException || ex.StatusCode == null)
|
catch (HttpRequestException ex) when (ex.InnerException is TimeoutException || ex.StatusCode == null)
|
||||||
{
|
{
|
||||||
|
response?.Dispose();
|
||||||
retryCount++;
|
retryCount++;
|
||||||
|
|
||||||
Logger.LogWarning(ex, "Timeout during download of {requestUrl}. Attempt {attempt} of {maxRetries}", requestUrl, retryCount, maxRetries);
|
Logger.LogWarning(ex, "Timeout during download of {requestUrl}. Attempt {attempt} of {maxRetries}", requestUrl, retryCount, maxRetries);
|
||||||
|
|
||||||
if (retryCount >= maxRetries || ct.IsCancellationRequested)
|
if (retryCount >= maxRetries || ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
Logger.LogError($"Max retries reached or cancelled. Failing download for {requestUrl}");
|
Logger.LogError("Max retries reached or cancelled. Failing download for {requestUrl}", requestUrl);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(retryDelay, ct).ConfigureAwait(false); // Wait before retrying
|
await Task.Delay(retryDelay, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
|
response?.Dispose();
|
||||||
Logger.LogWarning(ex, "Error during download of {requestUrl}, HttpStatusCode: {code}", requestUrl, ex.StatusCode);
|
Logger.LogWarning(ex, "Error during download of {requestUrl}, HttpStatusCode: {code}", requestUrl, ex.StatusCode);
|
||||||
|
|
||||||
if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized)
|
if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized)
|
||||||
@@ -196,42 +225,80 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
throw new InvalidDataException($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex);
|
throw new InvalidDataException($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ThrottledStream? stream = null;
|
ThrottledStream? stream = null;
|
||||||
FileStream? fileStream = null;
|
FileStream? fileStream = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
fileStream = File.Create(tempPath);
|
fileStream = File.Create(destinationFilename);
|
||||||
await using (fileStream.ConfigureAwait(false))
|
await using (fileStream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var bufferSize = response.Content.Headers.ContentLength > 1024 * 1024 ? 65536 : 8196;
|
var bufferSize = response!.Content.Headers.ContentLength > 1024 * 1024 ? 65536 : 8196;
|
||||||
var buffer = new byte[bufferSize];
|
var buffer = new byte[bufferSize];
|
||||||
|
|
||||||
var bytesRead = 0;
|
|
||||||
var limit = _orchestrator.DownloadLimitPerSlot();
|
var limit = _orchestrator.DownloadLimitPerSlot();
|
||||||
Logger.LogTrace("Starting Download of {id} with a speed limit of {limit} to {tempPath}", requestId, limit, tempPath);
|
Logger.LogTrace("Starting Download with a speed limit of {limit} to {destination}", limit, destinationFilename);
|
||||||
|
|
||||||
stream = new(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), limit);
|
stream = new(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), limit);
|
||||||
|
|
||||||
_activeDownloadStreams.TryAdd(stream, 0);
|
_activeDownloadStreams.TryAdd(stream, 0);
|
||||||
|
|
||||||
while ((bytesRead = await stream.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
|
while (true)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
int bytesRead;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var readTask = stream.ReadAsync(buffer.AsMemory(0, buffer.Length), ct).AsTask();
|
||||||
|
while (!readTask.IsCompleted)
|
||||||
|
{
|
||||||
|
var completedTask = await Task.WhenAny(readTask, Task.Delay(DownloadStallTimeout)).ConfigureAwait(false);
|
||||||
|
if (completedTask == readTask)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
MungeBuffer(buffer.AsSpan(0, bytesRead));
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var snapshot = _pairProcessingLimiter.GetSnapshot();
|
||||||
|
if (snapshot.Waiting > 0)
|
||||||
|
{
|
||||||
|
throw new TimeoutException($"No data received for {DownloadStallTimeout.TotalSeconds} seconds while downloading {requestUrl} (waiting: {snapshot.Waiting})");
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogTrace("Download stalled for {requestUrl} but no queued pairs, continuing to wait", requestUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesRead = await readTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback?.Invoke(buffer.AsSpan(0, bytesRead));
|
||||||
|
|
||||||
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), ct).ConfigureAwait(false);
|
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), ct).ConfigureAwait(false);
|
||||||
|
|
||||||
progress.Report(bytesRead);
|
progress.Report(bytesRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogDebug("{requestUrl} downloaded to {tempPath}", requestUrl, tempPath);
|
Logger.LogDebug("{requestUrl} downloaded to {destination}", requestUrl, destinationFilename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (TimeoutException ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Detected stalled download for {requestUrl}, aborting transfer", requestUrl);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
@@ -240,18 +307,18 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
fileStream?.Close();
|
fileStream?.Close();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(tempPath) && File.Exists(tempPath))
|
if (!string.IsNullOrEmpty(destinationFilename) && File.Exists(destinationFilename))
|
||||||
{
|
{
|
||||||
File.Delete(tempPath);
|
File.Delete(destinationFilename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Ignore errors during cleanup
|
// ignore cleanup errors
|
||||||
}
|
}
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -260,6 +327,134 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
_activeDownloadStreams.TryRemove(stream, out _);
|
_activeDownloadStreams.TryRemove(stream, out _);
|
||||||
await stream.DisposeAsync().ConfigureAwait(false);
|
await stream.DisposeAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DecompressBlockFileAsync(string downloadStatusKey, string blockFilePath, List<FileReplacementData> fileReplacement, string downloadLabel)
|
||||||
|
{
|
||||||
|
if (_downloadStatus.TryGetValue(downloadStatusKey, out var status))
|
||||||
|
{
|
||||||
|
status.TransferredFiles = 1;
|
||||||
|
status.DownloadStatus = DownloadStatus.Decompressing;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileStream? fileBlockStream = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fileBlockStream = File.OpenRead(blockFilePath);
|
||||||
|
while (fileBlockStream.Position < fileBlockStream.Length)
|
||||||
|
{
|
||||||
|
(string fileHash, long fileLengthBytes) = ReadBlockFileHeader(fileBlockStream);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fileExtension = fileReplacement.First(f => string.Equals(f.Hash, fileHash, StringComparison.OrdinalIgnoreCase)).GamePaths[0].Split(".")[^1];
|
||||||
|
var filePath = _fileDbManager.GetCacheFilePath(fileHash, fileExtension);
|
||||||
|
Logger.LogDebug("{dlName}: Decompressing {file}:{le} => {dest}", downloadLabel, fileHash, fileLengthBytes, filePath);
|
||||||
|
|
||||||
|
byte[] compressedFileContent = new byte[fileLengthBytes];
|
||||||
|
var readBytes = await fileBlockStream.ReadAsync(compressedFileContent, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (readBytes != fileLengthBytes)
|
||||||
|
{
|
||||||
|
throw new EndOfStreamException();
|
||||||
|
}
|
||||||
|
MungeBuffer(compressedFileContent);
|
||||||
|
|
||||||
|
var decompressedFile = LZ4Wrapper.Unwrap(compressedFileContent);
|
||||||
|
await _fileCompactor.WriteAllBytesAsync(filePath, decompressedFile, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
PersistFileToStorage(fileHash, filePath);
|
||||||
|
}
|
||||||
|
catch (EndOfStreamException)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("{dlName}: Failure to extract file {fileHash}, stream ended prematurely", downloadLabel, fileHash);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(e, "{dlName}: Error during decompression", downloadLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (EndOfStreamException)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("{dlName}: Failure to extract file header data, stream ended", downloadLabel);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "{dlName}: Error during block file read", downloadLabel);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (fileBlockStream != null)
|
||||||
|
await fileBlockStream.DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PerformDirectDownloadFallbackAsync(DownloadFileTransfer directDownload, List<FileReplacementData> fileReplacement,
|
||||||
|
IProgress<long> progress, CancellationToken token, bool slotAlreadyAcquired)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(directDownload.DirectDownloadUrl))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Direct download fallback requested without a direct download URL.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadKey = directDownload.DirectDownloadUrl!;
|
||||||
|
bool slotAcquiredHere = false;
|
||||||
|
string? blockFile = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!slotAlreadyAcquired)
|
||||||
|
{
|
||||||
|
if (_downloadStatus.TryGetValue(downloadKey, out var tracker))
|
||||||
|
{
|
||||||
|
tracker.DownloadStatus = DownloadStatus.WaitingForSlot;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
|
||||||
|
slotAcquiredHere = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_downloadStatus.TryGetValue(downloadKey, out var queueTracker))
|
||||||
|
{
|
||||||
|
queueTracker.DownloadStatus = DownloadStatus.WaitingForQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestIdResponse = await _orchestrator.SendRequestAsync(HttpMethod.Post, LightlessFiles.RequestEnqueueFullPath(directDownload.DownloadUri),
|
||||||
|
new[] { directDownload.Hash }, token).ConfigureAwait(false);
|
||||||
|
var requestId = Guid.Parse((await requestIdResponse.Content.ReadAsStringAsync().ConfigureAwait(false)).Trim('"'));
|
||||||
|
|
||||||
|
blockFile = _fileDbManager.GetCacheFilePath(requestId.ToString("N"), "blk");
|
||||||
|
|
||||||
|
await DownloadAndMungeFileHttpClient(downloadKey, requestId, [directDownload], blockFile, progress, token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!File.Exists(blockFile))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("Block file missing after direct download fallback.", blockFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
await DecompressBlockFileAsync(downloadKey, blockFile, fileReplacement, $"fallback-{directDownload.Hash}").ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (slotAcquiredHere)
|
||||||
|
{
|
||||||
|
_orchestrator.ReleaseDownloadSlot();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(blockFile))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(blockFile);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,30 +502,76 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private async Task DownloadFilesInternal(GameObjectHandler gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct)
|
private async Task DownloadFilesInternal(GameObjectHandler gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var downloadGroups = CurrentDownloads.GroupBy(f => f.DownloadUri.Host + ":" + f.DownloadUri.Port, StringComparer.Ordinal);
|
var objectName = gameObjectHandler?.Name ?? "Unknown";
|
||||||
|
|
||||||
foreach (var downloadGroup in downloadGroups)
|
var configAllowsDirect = _configService.Current.EnableDirectDownloads;
|
||||||
|
if (configAllowsDirect != _lastConfigDirectDownloadsState)
|
||||||
{
|
{
|
||||||
_downloadStatus[downloadGroup.Key] = new FileDownloadStatus()
|
_lastConfigDirectDownloadsState = configAllowsDirect;
|
||||||
|
if (configAllowsDirect)
|
||||||
|
{
|
||||||
|
_disableDirectDownloads = false;
|
||||||
|
_consecutiveDirectDownloadFailures = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowDirectDownloads = ShouldUseDirectDownloads();
|
||||||
|
|
||||||
|
var directDownloads = new List<DownloadFileTransfer>();
|
||||||
|
var batchDownloads = new List<DownloadFileTransfer>();
|
||||||
|
|
||||||
|
foreach (var download in CurrentDownloads)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(download.DirectDownloadUrl) && allowDirectDownloads)
|
||||||
|
{
|
||||||
|
directDownloads.Add(download);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
batchDownloads.Add(download);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadBatches = batchDownloads.GroupBy(f => f.DownloadUri.Host + ":" + f.DownloadUri.Port, StringComparer.Ordinal).ToArray();
|
||||||
|
|
||||||
|
foreach (var directDownload in directDownloads)
|
||||||
|
{
|
||||||
|
_downloadStatus[directDownload.DirectDownloadUrl!] = new FileDownloadStatus()
|
||||||
{
|
{
|
||||||
DownloadStatus = DownloadStatus.Initializing,
|
DownloadStatus = DownloadStatus.Initializing,
|
||||||
TotalBytes = downloadGroup.Sum(c => c.Total),
|
TotalBytes = directDownload.Total,
|
||||||
TotalFiles = 1,
|
TotalFiles = 1,
|
||||||
TransferredBytes = 0,
|
TransferredBytes = 0,
|
||||||
TransferredFiles = 0
|
TransferredFiles = 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var downloadBatch in downloadBatches)
|
||||||
|
{
|
||||||
|
_downloadStatus[downloadBatch.Key] = new FileDownloadStatus()
|
||||||
|
{
|
||||||
|
DownloadStatus = DownloadStatus.Initializing,
|
||||||
|
TotalBytes = downloadBatch.Sum(c => c.Total),
|
||||||
|
TotalFiles = 1,
|
||||||
|
TransferredBytes = 0,
|
||||||
|
TransferredFiles = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directDownloads.Count > 0 || downloadBatches.Length > 0)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Downloading {direct} files directly, and {batchtotal} in {batches} batches.", directDownloads.Count, batchDownloads.Count, downloadBatches.Length);
|
||||||
|
}
|
||||||
|
|
||||||
Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus));
|
Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus));
|
||||||
|
|
||||||
await Parallel.ForEachAsync(downloadGroups, new ParallelOptions()
|
Task batchDownloadsTask = downloadBatches.Length == 0 ? Task.CompletedTask : Parallel.ForEachAsync(downloadBatches, new ParallelOptions()
|
||||||
{
|
{
|
||||||
MaxDegreeOfParallelism = downloadGroups.Count(),
|
MaxDegreeOfParallelism = downloadBatches.Length,
|
||||||
CancellationToken = ct,
|
CancellationToken = ct,
|
||||||
},
|
},
|
||||||
async (fileGroup, token) =>
|
async (fileGroup, token) =>
|
||||||
{
|
{
|
||||||
// let server predownload files
|
|
||||||
var requestIdResponse = await _orchestrator.SendRequestAsync(HttpMethod.Post, LightlessFiles.RequestEnqueueFullPath(fileGroup.First().DownloadUri),
|
var requestIdResponse = await _orchestrator.SendRequestAsync(HttpMethod.Post, LightlessFiles.RequestEnqueueFullPath(fileGroup.First().DownloadUri),
|
||||||
fileGroup.Select(c => c.Hash), token).ConfigureAwait(false);
|
fileGroup.Select(c => c.Hash), token).ConfigureAwait(false);
|
||||||
Logger.LogDebug("Sent request for {n} files on server {uri} with result {result}", fileGroup.Count(), fileGroup.First().DownloadUri,
|
Logger.LogDebug("Sent request for {n} files on server {uri} with result {result}", fileGroup.Count(), fileGroup.First().DownloadUri,
|
||||||
@@ -353,7 +594,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
downloadStatus.DownloadStatus = DownloadStatus.WaitingForSlot;
|
downloadStatus.DownloadStatus = DownloadStatus.WaitingForSlot;
|
||||||
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
|
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
|
||||||
downloadStatus.DownloadStatus = DownloadStatus.WaitingForQueue;
|
downloadStatus.DownloadStatus = DownloadStatus.WaitingForQueue;
|
||||||
Progress<long> progress = new((bytesDownloaded) =>
|
var progress = CreateInlineProgress((bytesDownloaded) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -371,7 +612,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("{dlName}: Detected cancellation of download, partially extracting files for {id}", fi.Name, gameObjectHandler);
|
Logger.LogDebug("{dlName}: Detected cancellation of download, partially extracting files for {id}", fi.Name, objectName);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -382,72 +623,167 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
FileStream? fileBlockStream = null;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_downloadStatus.TryGetValue(fileGroup.Key, out var status))
|
|
||||||
{
|
|
||||||
status.TransferredFiles = 1;
|
|
||||||
status.DownloadStatus = DownloadStatus.Decompressing;
|
|
||||||
}
|
|
||||||
if (!File.Exists(blockFile))
|
if (!File.Exists(blockFile))
|
||||||
{
|
{
|
||||||
Logger.LogWarning("{dlName}: Block file missing before extraction, skipping", fi.Name);
|
Logger.LogWarning("{dlName}: Block file missing before extraction, skipping", fi.Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fileBlockStream = File.OpenRead(blockFile);
|
await DecompressBlockFileAsync(fileGroup.Key, blockFile, fileReplacement, fi.Name).ConfigureAwait(false);
|
||||||
while (fileBlockStream.Position < fileBlockStream.Length)
|
|
||||||
{
|
|
||||||
(string fileHash, long fileLengthBytes) = ReadBlockFileHeader(fileBlockStream);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fileExtension = fileReplacement.First(f => string.Equals(f.Hash, fileHash, StringComparison.OrdinalIgnoreCase)).GamePaths[0].Split(".")[^1];
|
|
||||||
var filePath = _fileDbManager.GetCacheFilePath(fileHash, fileExtension);
|
|
||||||
Logger.LogDebug("{dlName}: Decompressing {file}:{le} => {dest}", fi.Name, fileHash, fileLengthBytes, filePath);
|
|
||||||
|
|
||||||
byte[] compressedFileContent = new byte[fileLengthBytes];
|
|
||||||
var readBytes = await fileBlockStream.ReadAsync(compressedFileContent, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
if (readBytes != fileLengthBytes)
|
|
||||||
{
|
|
||||||
throw new EndOfStreamException();
|
|
||||||
}
|
|
||||||
MungeBuffer(compressedFileContent);
|
|
||||||
|
|
||||||
var decompressedFile = LZ4Wrapper.Unwrap(compressedFileContent);
|
|
||||||
await _fileCompactor.WriteAllBytesAsync(filePath, decompressedFile, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
|
|
||||||
PersistFileToStorage(fileHash, filePath);
|
|
||||||
}
|
|
||||||
catch (EndOfStreamException)
|
|
||||||
{
|
|
||||||
Logger.LogWarning("{dlName}: Failure to extract file {fileHash}, stream ended prematurely", fi.Name, fileHash);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogWarning(e, "{dlName}: Error during decompression", fi.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (EndOfStreamException)
|
|
||||||
{
|
|
||||||
Logger.LogDebug("{dlName}: Failure to extract file header data, stream ended", fi.Name);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "{dlName}: Error during block file read", fi.Name);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_orchestrator.ReleaseDownloadSlot();
|
_orchestrator.ReleaseDownloadSlot();
|
||||||
if (fileBlockStream != null)
|
|
||||||
await fileBlockStream.DisposeAsync().ConfigureAwait(false);
|
|
||||||
File.Delete(blockFile);
|
File.Delete(blockFile);
|
||||||
}
|
}
|
||||||
}).ConfigureAwait(false);
|
});
|
||||||
|
|
||||||
Logger.LogDebug("Download end: {id}", gameObjectHandler);
|
Task directDownloadsTask = directDownloads.Count == 0 ? Task.CompletedTask : Parallel.ForEachAsync(directDownloads, new ParallelOptions()
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = directDownloads.Count,
|
||||||
|
CancellationToken = ct,
|
||||||
|
},
|
||||||
|
async (directDownload, token) =>
|
||||||
|
{
|
||||||
|
if (!_downloadStatus.TryGetValue(directDownload.DirectDownloadUrl!, out var downloadTracker))
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Download status missing for direct URL {url}", directDownload.DirectDownloadUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress = CreateInlineProgress((bytesDownloaded) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_downloadStatus.TryGetValue(directDownload.DirectDownloadUrl!, out FileDownloadStatus? value))
|
||||||
|
{
|
||||||
|
value.TransferredBytes += bytesDownloaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Could not set download progress");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ShouldUseDirectDownloads())
|
||||||
|
{
|
||||||
|
await PerformDirectDownloadFallbackAsync(directDownload, fileReplacement, progress, token, slotAlreadyAcquired: false).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tempFilename = _fileDbManager.GetCacheFilePath(directDownload.Hash, "bin");
|
||||||
|
var slotAcquired = false;
|
||||||
|
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
downloadTracker.DownloadStatus = DownloadStatus.WaitingForSlot;
|
||||||
|
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
|
||||||
|
slotAcquired = true;
|
||||||
|
|
||||||
|
downloadTracker.DownloadStatus = DownloadStatus.Downloading;
|
||||||
|
Logger.LogDebug("Beginning direct download of {hash} from {url}", directDownload.Hash, directDownload.DirectDownloadUrl);
|
||||||
|
await DownloadFileThrottled(new Uri(directDownload.DirectDownloadUrl!), tempFilename, progress, null, token, withToken: false).ConfigureAwait(false);
|
||||||
|
|
||||||
|
Interlocked.Exchange(ref _consecutiveDirectDownloadFailures, 0);
|
||||||
|
|
||||||
|
downloadTracker.DownloadStatus = DownloadStatus.Decompressing;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var replacement = fileReplacement.FirstOrDefault(f => string.Equals(f.Hash, directDownload.Hash, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (replacement == null || replacement.GamePaths.Length == 0)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("{hash}: No replacement data found for direct download.", directDownload.Hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileExtension = replacement.GamePaths[0].Split(".")[^1];
|
||||||
|
var finalFilename = _fileDbManager.GetCacheFilePath(directDownload.Hash, fileExtension);
|
||||||
|
Logger.LogDebug("Decompressing direct download {hash} from {compressedFile} to {finalFile}", directDownload.Hash, tempFilename, finalFilename);
|
||||||
|
byte[] compressedBytes = await File.ReadAllBytesAsync(tempFilename).ConfigureAwait(false);
|
||||||
|
var decompressedBytes = LZ4Wrapper.Unwrap(compressedBytes);
|
||||||
|
await _fileCompactor.WriteAllBytesAsync(finalFilename, decompressedBytes, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
PersistFileToStorage(directDownload.Hash, finalFilename);
|
||||||
|
|
||||||
|
downloadTracker.TransferredFiles = 1;
|
||||||
|
Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Exception downloading {hash} from {url}", directDownload.Hash, directDownload.DirectDownloadUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("{hash}: Detected cancellation of direct download, discarding file.", directDownload.Hash);
|
||||||
|
Logger.LogError(ex, "{hash}: Error during direct download.", directDownload.Hash);
|
||||||
|
ClearDownload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var expectedDirectDownloadFailure = ex is InvalidDataException;
|
||||||
|
var failureCount = 0;
|
||||||
|
|
||||||
|
if (expectedDirectDownloadFailure)
|
||||||
|
{
|
||||||
|
Logger.LogInformation(ex, "{hash}: Direct download unavailable, attempting queued fallback.", directDownload.Hash);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
failureCount = Interlocked.Increment(ref _consecutiveDirectDownloadFailures);
|
||||||
|
Logger.LogWarning(ex, "{hash}: Direct download failed, attempting queued fallback.", directDownload.Hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
downloadTracker.DownloadStatus = DownloadStatus.WaitingForQueue;
|
||||||
|
await PerformDirectDownloadFallbackAsync(directDownload, fileReplacement, progress, token, slotAcquired).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!expectedDirectDownloadFailure && failureCount >= 3 && !_disableDirectDownloads)
|
||||||
|
{
|
||||||
|
_disableDirectDownloads = true;
|
||||||
|
Logger.LogWarning("Disabling direct downloads for this session after {count} consecutive failures.", failureCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception fallbackEx)
|
||||||
|
{
|
||||||
|
if (slotAcquired)
|
||||||
|
{
|
||||||
|
_orchestrator.ReleaseDownloadSlot();
|
||||||
|
slotAcquired = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogError(fallbackEx, "{hash}: Error during direct download fallback.", directDownload.Hash);
|
||||||
|
ClearDownload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (slotAcquired)
|
||||||
|
{
|
||||||
|
_orchestrator.ReleaseDownloadSlot();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(tempFilename);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(batchDownloadsTask, directDownloadsTask).ConfigureAwait(false);
|
||||||
|
|
||||||
|
Logger.LogDebug("Download end: {id}", objectName);
|
||||||
|
|
||||||
ClearDownload();
|
ClearDownload();
|
||||||
}
|
}
|
||||||
@@ -554,4 +890,24 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
_orchestrator.ClearDownloadRequest(requestId);
|
_orchestrator.ClearDownloadRequest(requestId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private static IProgress<long> CreateInlineProgress(Action<long> callback)
|
||||||
|
{
|
||||||
|
return new InlineProgress(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class InlineProgress : IProgress<long>
|
||||||
|
{
|
||||||
|
private readonly Action<long> _callback;
|
||||||
|
|
||||||
|
public InlineProgress(Action<long> callback)
|
||||||
|
{
|
||||||
|
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Report(long value)
|
||||||
|
{
|
||||||
|
_callback(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -81,27 +81,30 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> SendRequestAsync(HttpMethod method, Uri uri,
|
public async Task<HttpResponseMessage> SendRequestAsync(HttpMethod method, Uri uri,
|
||||||
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
|
||||||
|
bool withToken = true)
|
||||||
{
|
{
|
||||||
using var requestMessage = new HttpRequestMessage(method, uri);
|
using var requestMessage = new HttpRequestMessage(method, uri);
|
||||||
return await SendRequestInternalAsync(requestMessage, ct, httpCompletionOption).ConfigureAwait(false);
|
return await SendRequestInternalAsync(requestMessage, ct, httpCompletionOption, withToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct) where T : class
|
public async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct,
|
||||||
|
bool withToken = true) where T : class
|
||||||
{
|
{
|
||||||
using var requestMessage = new HttpRequestMessage(method, uri);
|
using var requestMessage = new HttpRequestMessage(method, uri);
|
||||||
if (content is not ByteArrayContent)
|
if (content is not ByteArrayContent)
|
||||||
requestMessage.Content = JsonContent.Create(content);
|
requestMessage.Content = JsonContent.Create(content);
|
||||||
else
|
else
|
||||||
requestMessage.Content = content as ByteArrayContent;
|
requestMessage.Content = content as ByteArrayContent;
|
||||||
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
|
return await SendRequestInternalAsync(requestMessage, ct, withToken: withToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content, CancellationToken ct)
|
public async Task<HttpResponseMessage> SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content,
|
||||||
|
CancellationToken ct, bool withToken = true)
|
||||||
{
|
{
|
||||||
using var requestMessage = new HttpRequestMessage(method, uri);
|
using var requestMessage = new HttpRequestMessage(method, uri);
|
||||||
requestMessage.Content = content;
|
requestMessage.Content = content;
|
||||||
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
|
return await SendRequestInternalAsync(requestMessage, ct, withToken: withToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task WaitForDownloadSlotAsync(CancellationToken token)
|
public async Task WaitForDownloadSlotAsync(CancellationToken token)
|
||||||
@@ -144,10 +147,13 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<HttpResponseMessage> SendRequestInternalAsync(HttpRequestMessage requestMessage,
|
private async Task<HttpResponseMessage> SendRequestInternalAsync(HttpRequestMessage requestMessage,
|
||||||
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, bool withToken = true)
|
||||||
{
|
{
|
||||||
var token = await _tokenProvider.GetToken().ConfigureAwait(false);
|
if (withToken)
|
||||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
{
|
||||||
|
var token = await _tokenProvider.GetToken().ConfigureAwait(false);
|
||||||
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||||
|
}
|
||||||
|
|
||||||
if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent)
|
if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class DownloadFileTransfer : FileTransfer
|
|||||||
}
|
}
|
||||||
get => Dto.Size;
|
get => Dto.Size;
|
||||||
}
|
}
|
||||||
|
public string? DirectDownloadUrl => ((DownloadFileDto)TransferDto).CDNDownloadUrl;
|
||||||
|
|
||||||
public long TotalRaw => Dto.RawSize;
|
public long TotalRaw => Dto.RawSize;
|
||||||
private DownloadFileDto Dto => (DownloadFileDto)TransferDto;
|
private DownloadFileDto Dto => (DownloadFileDto)TransferDto;
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ public partial class ApiController
|
|||||||
|
|
||||||
public async Task<UserProfileDto> UserGetProfile(UserDto dto)
|
public async Task<UserProfileDto> UserGetProfile(UserDto dto)
|
||||||
{
|
{
|
||||||
if (!IsConnected) return new UserProfileDto(dto.User, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, Description: null);
|
if (!IsConnected) return new UserProfileDto(dto.User, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, Description: null, Tags: null);
|
||||||
return await _lightlessHub!.InvokeAsync<UserProfileDto>(nameof(UserGetProfile), dto).ConfigureAwait(false);
|
return await _lightlessHub!.InvokeAsync<UserProfileDto>(nameof(UserGetProfile), dto).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,14 @@ public partial class ApiController
|
|||||||
public Task Client_UserUpdateProfile(UserDto dto)
|
public Task Client_UserUpdateProfile(UserDto dto)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Client_UserUpdateProfile: {dto}", dto);
|
Logger.LogDebug("Client_UserUpdateProfile: {dto}", dto);
|
||||||
ExecuteSafely(() => Mediator.Publish(new ClearProfileDataMessage(dto.User)));
|
ExecuteSafely(() => Mediator.Publish(new ClearProfileUserDataMessage(dto.User)));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Client_GroupSendProfile(GroupProfileDto groupInfo)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Client_GroupSendProfile: {dto}", groupInfo);
|
||||||
|
ExecuteSafely(() => Mediator.Publish(new ClearProfileGroupDataMessage(groupInfo.Group)));
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,6 +387,12 @@ public partial class ApiController
|
|||||||
_lightlessHub!.On(nameof(Client_UserUpdateProfile), act);
|
_lightlessHub!.On(nameof(Client_UserUpdateProfile), act);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ClientGroupSendProfile(Action<GroupProfileDto> act)
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
_lightlessHub!.On(nameof(Client_GroupSendProfile), act);
|
||||||
|
}
|
||||||
|
|
||||||
public void OnUserUpdateSelfPairPermissions(Action<UserPermissionsDto> act)
|
public void OnUserUpdateSelfPairPermissions(Action<UserPermissionsDto> act)
|
||||||
{
|
{
|
||||||
if (_initialized) return;
|
if (_initialized) return;
|
||||||
|
|||||||
@@ -115,6 +115,18 @@ public partial class ApiController
|
|||||||
CheckConnection();
|
CheckConnection();
|
||||||
return await _lightlessHub!.InvokeAsync<int>(nameof(GroupPrune), group, days, execute).ConfigureAwait(false);
|
return await _lightlessHub!.InvokeAsync<int>(nameof(GroupPrune), group, days, execute).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
public async Task<GroupProfileDto> GroupGetProfile(GroupDto dto)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
if (!IsConnected) return new GroupProfileDto(Group: dto.Group, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false);
|
||||||
|
return await _lightlessHub!.InvokeAsync<GroupProfileDto>(nameof(GroupGetProfile), dto).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task GroupSetProfile(GroupProfileDto dto)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
await _lightlessHub!.InvokeAsync(nameof(GroupSetProfile), dto).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<GroupFullInfoDto>> GroupsGetAll()
|
public async Task<List<GroupFullInfoDto>> GroupsGetAll()
|
||||||
{
|
{
|
||||||
@@ -139,7 +151,6 @@ public partial class ApiController
|
|||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void CheckConnection()
|
private void CheckConnection()
|
||||||
{
|
{
|
||||||
if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new InvalidDataException("Not connected");
|
if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new InvalidDataException("Not connected");
|
||||||
|
|||||||
@@ -608,17 +608,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
ServerState = state;
|
ServerState = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Client_GroupSendProfile(GroupProfileDto groupInfo)
|
public Task<UserProfileDto?> UserGetLightfinderProfile(string hashedCid)
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<GroupProfileDto> GroupGetProfile(GroupDto dto)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task GroupSetProfile(GroupProfileDto dto)
|
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user