[Draft] Update 0.8 (#46)

* move stuff out into file transfer manager

* obnoxious unsupported version text, adjustments to filetransfermanager

* add back file upload transfer progress

* restructure code

* cleanup some more stuff I guess

* downloadids by playername

* individual anim/sound bs

* fix migration stuff, finalize impl of individual sound/anim pause

* fixes with logging stuff

* move download manager to transient

* rework dl ui first iteration

* some refactoring and cleanup

* more code cleanup

* refactoring

* switch to hostbuilder

* some more rework I guess

* more refactoring

* clean up mediator calls and disposal

* fun code cleanup

* push error message when log level is set to anything but information in non-debug builds

* remove notificationservice

* move message to after login

* add download bars to gameworld

* fixes download progress bar

* set gpose ui min and max size

* remove unnecessary usings

* adjustments to reconnection logic

* add options to set visible/offline groups visibility

* add impl of uploading display, transfer list in settings ui

* attempt to fix issues with server selection

* add back download status to compact ui

* make dl bar fixed size based

* some fixes for upload/download handling

* adjust text from Syncing back to Uploading

---------

Co-authored-by: rootdarkarchon <root.darkarchon@outlook.com>
Co-authored-by: Stanley Dimant <stanley.dimant@varian.com>
This commit is contained in:
rootdarkarchon
2023-03-14 19:48:35 +01:00
committed by GitHub
parent 0824ba434b
commit 0c87e84f25
109 changed files with 7323 additions and 6488 deletions

View File

@@ -1,540 +0,0 @@
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Text;
using Dalamud.Utility;
using LZ4;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Files;
using MareSynchronos.API.Routes;
using MareSynchronos.Mediator;
using MareSynchronos.UI;
using MareSynchronos.WebAPI.Utils;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.WebAPI;
public partial class ApiController
{
private readonly Dictionary<string, DateTime> _verifiedUploadedHashes;
private readonly ConcurrentDictionary<Guid, bool> _downloadReady = new();
private bool _currentUploadCancelled = false;
private int _downloadId = 0;
public async Task<bool> CancelUpload()
{
if (CurrentUploads.Any())
{
_logger.LogDebug("Cancelling current upload");
_uploadCancellationTokenSource?.Cancel();
_uploadCancellationTokenSource?.Dispose();
_uploadCancellationTokenSource = null;
CurrentUploads.Clear();
await FilesAbortUpload().ConfigureAwait(false);
return true;
}
return false;
}
public async Task FilesAbortUpload()
{
await _mareHub!.SendAsync(nameof(FilesAbortUpload)).ConfigureAwait(false);
}
public async Task FilesDeleteAll()
{
_verifiedUploadedHashes.Clear();
await _mareHub!.SendAsync(nameof(FilesDeleteAll)).ConfigureAwait(false);
}
private async Task<Guid> GetQueueRequest(DownloadFileTransfer downloadFileTransfer, CancellationToken ct)
{
var response = await SendRequestAsync(HttpMethod.Get, MareFiles.RequestRequestFileFullPath(downloadFileTransfer.DownloadUri, downloadFileTransfer.Hash), ct).ConfigureAwait(false);
var responseString = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
var requestId = Guid.Parse(responseString.Trim('"'));
if (!_downloadReady.ContainsKey(requestId))
{
_downloadReady[requestId] = false;
}
return requestId;
}
private async Task WaitForDownloadReady(DownloadFileTransfer downloadFileTransfer, Guid requestId, CancellationToken downloadCt)
{
bool alreadyCancelled = false;
try
{
CancellationTokenSource localTimeoutCts = new();
localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5));
CancellationTokenSource composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token);
while (_downloadReady.TryGetValue(requestId, out bool isReady) && !isReady)
{
try
{
await Task.Delay(250, composite.Token).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
if (downloadCt.IsCancellationRequested) throw;
var req = await SendRequestAsync(HttpMethod.Get, MareFiles.RequestCheckQueueFullPath(downloadFileTransfer.DownloadUri, requestId, downloadFileTransfer.Hash), downloadCt).ConfigureAwait(false);
try
{
req.EnsureSuccessStatusCode();
localTimeoutCts.Dispose();
composite.Dispose();
localTimeoutCts = new();
localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5));
composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token);
}
catch (HttpRequestException)
{
throw;
}
}
}
localTimeoutCts.Dispose();
composite.Dispose();
_logger.LogDebug($"Download {requestId} ready");
}
catch (TaskCanceledException)
{
try
{
await SendRequestAsync(HttpMethod.Get, MareFiles.RequestCancelFullPath(downloadFileTransfer.DownloadUri, requestId)).ConfigureAwait(false);
alreadyCancelled = true;
}
catch { }
throw;
}
finally
{
if (downloadCt.IsCancellationRequested && !alreadyCancelled)
{
try
{
await SendRequestAsync(HttpMethod.Get, MareFiles.RequestCancelFullPath(downloadFileTransfer.DownloadUri, requestId)).ConfigureAwait(false);
}
catch { }
}
_downloadReady.Remove(requestId, out _);
}
}
private async Task DownloadFileHttpClient(DownloadFileTransfer fileTransfer, string tempPath, IProgress<long> progress, CancellationToken ct)
{
var requestId = await GetQueueRequest(fileTransfer, ct).ConfigureAwait(false);
_logger.LogDebug($"GUID {requestId} for file {fileTransfer.Hash} on server {fileTransfer.DownloadUri}");
await WaitForDownloadReady(fileTransfer, requestId, ct).ConfigureAwait(false);
HttpResponseMessage response = null!;
var requestUrl = MareFiles.CacheGetFullPath(fileTransfer.DownloadUri, requestId);
_logger.LogDebug($"Downloading {requestUrl} for file {fileTransfer.Hash}");
try
{
response = await SendRequestAsync(HttpMethod.Get, requestUrl, ct).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
catch (HttpRequestException ex)
{
_logger.LogWarning(ex, $"Error during download of {requestUrl}, HttpStatusCode: {ex.StatusCode}");
if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized)
{
throw new Exception($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex);
}
}
try
{
var fileStream = File.Create(tempPath);
await using (fileStream.ConfigureAwait(false))
{
var bufferSize = response.Content.Headers.ContentLength > 1024 * 1024 ? 4096 : 1024;
var buffer = new byte[bufferSize];
var bytesRead = 0;
while ((bytesRead = await (await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false)).ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
{
ct.ThrowIfCancellationRequested();
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), ct).ConfigureAwait(false);
progress.Report(bytesRead);
}
_logger.LogDebug($"{requestUrl} downloaded to {tempPath}");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Error during file download of {requestUrl}");
try
{
if (!tempPath.IsNullOrEmpty())
File.Delete(tempPath);
}
catch { }
throw;
}
}
public int GetDownloadId() => _downloadId++;
public async Task DownloadFiles(int currentDownloadId, List<FileReplacementData> fileReplacementDto, CancellationToken ct)
{
Mediator.Publish(new HaltScanMessage("Download"));
try
{
await DownloadFilesInternal(currentDownloadId, fileReplacementDto, ct).ConfigureAwait(false);
}
catch
{
CancelDownload(currentDownloadId);
}
finally
{
Mediator.Publish(new ResumeScanMessage("Download"));
}
}
private async Task<HttpResponseMessage> SendRequestAsync(HttpMethod method, Uri uri, CancellationToken? ct = null)
{
using var requestMessage = new HttpRequestMessage(method, uri);
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
}
private async Task<HttpResponseMessage> SendRequestInternalAsync(HttpRequestMessage requestMessage, CancellationToken? ct = null)
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this._serverManager.GetToken());
if (requestMessage.Content != null)
{
_logger.LogDebug("Sending " + requestMessage.Method + " to " + requestMessage.RequestUri + " (Content: " + await (((JsonContent)requestMessage.Content).ReadAsStringAsync()) + ")");
}
else
{
_logger.LogDebug("Sending " + requestMessage.Method + " to " + requestMessage.RequestUri);
}
try
{
if (ct != null)
return await _httpClient.SendAsync(requestMessage, ct.Value).ConfigureAwait(false);
return await _httpClient.SendAsync(requestMessage).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Error during SendRequestInternal for " + requestMessage.RequestUri);
throw;
}
}
private async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct) where T : class
{
using var requestMessage = new HttpRequestMessage(method, uri);
requestMessage.Content = JsonContent.Create(content);
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
}
private async Task DownloadFilesInternal(int currentDownloadId, List<FileReplacementData> fileReplacement, CancellationToken ct)
{
_logger.LogDebug("Downloading files (Download ID " + currentDownloadId + ")");
List<DownloadFileDto> downloadFileInfoFromService = new();
downloadFileInfoFromService.AddRange(await FilesGetSizes(fileReplacement.Select(f => f.Hash).ToList()).ConfigureAwait(false));
_logger.LogDebug("Files with size 0 or less: " + string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash)));
CurrentDownloads[currentDownloadId] = downloadFileInfoFromService.Distinct().Select(d => new DownloadFileTransfer(d))
.Where(d => d.CanBeTransferred).ToList();
foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden))
{
if (!ForbiddenTransfers.Any(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal)))
{
ForbiddenTransfers.Add(new DownloadFileTransfer(dto));
}
}
var downloadGroups = CurrentDownloads[currentDownloadId].Where(f => f.CanBeTransferred).GroupBy(f => f.DownloadUri.Host + f.DownloadUri.Port, StringComparer.Ordinal);
await Parallel.ForEachAsync(downloadGroups, new ParallelOptions()
{
MaxDegreeOfParallelism = downloadGroups.Count(),
CancellationToken = ct,
},
async (fileGroup, token) =>
{
// let server predownload files
await SendRequestAsync(HttpMethod.Post, MareFiles.RequestEnqueueFullPath(fileGroup.First().DownloadUri),
fileGroup.Select(c => c.Hash), token).ConfigureAwait(false);
foreach (var file in fileGroup)
{
var hash = file.Hash;
Progress<long> progress = new((bytesDownloaded) =>
{
file.Transferred += bytesDownloaded;
});
var tempPath = Path.Combine(_configService.Current.CacheFolder, file.Hash + ".tmp");
try
{
await DownloadFileHttpClient(file, tempPath, progress, token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
File.Delete(tempPath);
_logger.LogDebug("Detected cancellation, removing " + currentDownloadId);
CancelDownload(currentDownloadId);
return;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during download of " + file.Hash);
return;
}
var tempFileData = await File.ReadAllBytesAsync(tempPath, token).ConfigureAwait(false);
var extratokenedFile = LZ4Codec.Unwrap(tempFileData);
File.Delete(tempPath);
var filePath = Path.Combine(_configService.Current.CacheFolder, file.Hash);
await File.WriteAllBytesAsync(filePath, extratokenedFile, token).ConfigureAwait(false);
var fi = new FileInfo(filePath);
Func<DateTime> RandomDayInThePast()
{
DateTime start = new(1995, 1, 1);
Random gen = new();
int range = (DateTime.Today - start).Days;
return () => start.AddDays(gen.Next(range));
}
fi.CreationTime = RandomDayInThePast().Invoke();
fi.LastAccessTime = DateTime.Today;
fi.LastWriteTime = RandomDayInThePast().Invoke();
try
{
_ = _fileDbManager.CreateCacheEntry(filePath);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Issue creating cache entry");
}
}
}).ConfigureAwait(false);
_logger.LogDebug("Download complete, removing " + currentDownloadId);
CancelDownload(currentDownloadId);
}
public async Task PushCharacterData(CharacterData data, List<UserData> visibleCharacters)
{
if (!IsConnected) return;
try
{
_currentUploadCancelled = await CancelUpload().ConfigureAwait(false);
_uploadCancellationTokenSource = new CancellationTokenSource();
var uploadToken = _uploadCancellationTokenSource.Token;
_logger.LogDebug($"Sending Character data {data.DataHash.Value} to service {_serverManager.CurrentApiUrl}");
HashSet<string> unverifiedUploads = VerifyFiles(data);
if (unverifiedUploads.Any())
{
await UploadMissingFiles(unverifiedUploads, uploadToken).ConfigureAwait(false);
_logger.LogInformation("Upload complete for " + data.DataHash.Value);
}
await PushCharacterDataInternal(data, visibleCharacters.ToList()).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
_logger.LogDebug("Upload operation was cancelled");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error during upload of files");
}
finally
{
if (!_currentUploadCancelled)
_currentUploadCancelled = await CancelUpload().ConfigureAwait(false);
}
}
private HashSet<string> VerifyFiles(CharacterData data)
{
HashSet<string> unverifiedUploadHashes = new(StringComparer.Ordinal);
foreach (var item in data.FileReplacements.SelectMany(c => c.Value.Where(f => string.IsNullOrEmpty(f.FileSwapPath)).Select(v => v.Hash).Distinct(StringComparer.Ordinal)).Distinct(StringComparer.Ordinal).ToList())
{
if (!_verifiedUploadedHashes.TryGetValue(item, out var verifiedTime))
{
verifiedTime = DateTime.MinValue;
}
if (verifiedTime < DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(10)))
{
_logger.LogTrace("Verifying " + item + ", last verified: " + verifiedTime);
unverifiedUploadHashes.Add(item);
}
}
return unverifiedUploadHashes;
}
private async Task UploadMissingFiles(HashSet<string> unverifiedUploadHashes, CancellationToken uploadToken)
{
unverifiedUploadHashes = unverifiedUploadHashes.Where(h => _fileDbManager.GetFileCacheByHash(h) != null).ToHashSet(StringComparer.Ordinal);
_logger.LogDebug("Verifying " + unverifiedUploadHashes.Count + " files");
var filesToUpload = await FilesSend(unverifiedUploadHashes.ToList()).ConfigureAwait(false);
foreach (var file in filesToUpload.Where(f => !f.IsForbidden))
{
try
{
CurrentUploads.Add(new UploadFileTransfer(file)
{
Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length,
});
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Tried to request file " + file.Hash + " but file was not present");
}
}
foreach (var file in filesToUpload.Where(c => c.IsForbidden))
{
if (ForbiddenTransfers.All(f => !string.Equals(f.Hash, file.Hash, StringComparison.Ordinal)))
{
ForbiddenTransfers.Add(new UploadFileTransfer(file)
{
LocalFile = _fileDbManager.GetFileCacheByHash(file.Hash)?.ResolvedFilepath ?? string.Empty,
});
}
_verifiedUploadedHashes[file.Hash] = DateTime.UtcNow;
}
var totalSize = CurrentUploads.Sum(c => c.Total);
_logger.LogDebug("Compressing and uploading files");
foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList())
{
_logger.LogDebug("Compressing and uploading " + file);
var data = await GetCompressedFileData(file.Hash, uploadToken).ConfigureAwait(false);
CurrentUploads.Single(e => string.Equals(e.Hash, data.Item1, StringComparison.Ordinal)).Total = data.Item2.Length;
await UploadFile(data.Item2, file.Hash, uploadToken).ConfigureAwait(false);
_verifiedUploadedHashes[file.Hash] = DateTime.UtcNow;
uploadToken.ThrowIfCancellationRequested();
}
if (CurrentUploads.Any())
{
var compressedSize = CurrentUploads.Sum(c => c.Total);
_logger.LogDebug($"Compressed {UiShared.ByteToString(totalSize)} to {UiShared.ByteToString(compressedSize)} ({(compressedSize / (double)totalSize):P2})");
_logger.LogDebug("Upload tasks complete, waiting for server to confirm");
_logger.LogDebug("Uploads open: " + CurrentUploads.Any(c => c.IsInTransfer));
const double waitStep = 1.0d;
while (CurrentUploads.Any(c => c.IsInTransfer) && !uploadToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(waitStep), uploadToken).ConfigureAwait(false);
_logger.LogDebug("Waiting for uploads to finish");
}
}
foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Any(u => string.Equals(u.Hash, c, StringComparison.Ordinal))))
{
_verifiedUploadedHashes[file] = DateTime.UtcNow;
}
CurrentUploads.Clear();
}
private async Task PushCharacterDataInternal(CharacterData character, List<UserData> visibleCharacters)
{
_logger.LogInformation("Pushing character data for " + character.DataHash.Value + " to " + string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID)));
StringBuilder sb = new();
foreach (var kvp in character.FileReplacements.ToList())
{
sb.AppendLine($"FileReplacements for {kvp.Key}: {kvp.Value.Count}");
character.FileReplacements[kvp.Key].RemoveAll(i => ForbiddenTransfers.Any(f => string.Equals(f.Hash, i.Hash, StringComparison.OrdinalIgnoreCase)));
}
foreach (var item in character.GlamourerData)
{
sb.AppendLine($"GlamourerData for {item.Key}: {!string.IsNullOrEmpty(item.Value)}");
}
_logger.LogDebug("Chara data contained: " + Environment.NewLine + sb.ToString());
await UserPushData(new(visibleCharacters, character)).ConfigureAwait(false);
}
private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken)
{
var fileCache = _fileDbManager.GetFileCacheByHash(fileHash)!.ResolvedFilepath;
return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0,
(int)new FileInfo(fileCache).Length));
}
private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken)
{
if (uploadToken.IsCancellationRequested) return;
async IAsyncEnumerable<byte[]> AsyncFileData([EnumeratorCancellation] CancellationToken token)
{
var chunkSize = 1024 * 512; // 512kb
using var ms = new MemoryStream(compressedFile);
var buffer = new byte[chunkSize];
int bytesRead;
while ((bytesRead = await ms.ReadAsync(buffer, 0, chunkSize, token).ConfigureAwait(false)) > 0 && !token.IsCancellationRequested)
{
CurrentUploads.Single(f => string.Equals(f.Hash, fileHash, StringComparison.Ordinal)).Transferred += bytesRead;
token.ThrowIfCancellationRequested();
yield return bytesRead == chunkSize ? buffer.ToArray() : buffer.Take(bytesRead).ToArray();
}
}
await FilesUploadStreamAsync(fileHash, AsyncFileData(uploadToken)).ConfigureAwait(false);
}
public async Task FilesUploadStreamAsync(string hash, IAsyncEnumerable<byte[]> fileContent)
{
await _mareHub!.InvokeAsync(nameof(FilesUploadStreamAsync), hash, fileContent).ConfigureAwait(false);
}
public async Task<bool> FilesIsUploadFinished()
{
return await _mareHub!.InvokeAsync<bool>(nameof(FilesIsUploadFinished)).ConfigureAwait(false);
}
public async Task<List<DownloadFileDto>> FilesGetSizes(List<string> hashes)
{
return await _mareHub!.InvokeAsync<List<DownloadFileDto>>(nameof(FilesGetSizes), hashes).ConfigureAwait(false);
}
public async Task<List<UploadFileDto>> FilesSend(List<string> fileListHashes)
{
return await _mareHub!.InvokeAsync<List<UploadFileDto>>(nameof(FilesSend), fileListHashes).ConfigureAwait(false);
}
public void CancelDownload(int downloadId)
{
while (CurrentDownloads.ContainsKey(downloadId))
{
CurrentDownloads.TryRemove(downloadId, out _);
}
}
}

View File

@@ -1,55 +0,0 @@
using MareSynchronos.API.Dto.User;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.WebAPI;
public partial class ApiController
{
public async Task UserDelete()
{
CheckConnection();
await FilesDeleteAll().ConfigureAwait(false);
await _mareHub!.SendAsync(nameof(UserDelete)).ConfigureAwait(false);
await CreateConnections().ConfigureAwait(false);
}
public async Task UserPushData(UserCharaDataMessageDto dto)
{
try
{
await _mareHub!.InvokeAsync(nameof(UserPushData), dto).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to Push character data");
}
}
public async Task<List<UserPairDto>> UserGetPairedClients()
{
return await _mareHub!.InvokeAsync<List<UserPairDto>>(nameof(UserGetPairedClients)).ConfigureAwait(false);
}
public async Task<List<OnlineUserIdentDto>> UserGetOnlinePairs()
{
return await _mareHub!.InvokeAsync<List<OnlineUserIdentDto>>(nameof(UserGetOnlinePairs)).ConfigureAwait(false);
}
public async Task UserSetPairPermissions(UserPermissionsDto dto)
{
await _mareHub!.SendAsync(nameof(UserSetPairPermissions), dto).ConfigureAwait(false);
}
public async Task UserAddPair(UserDto dto)
{
if (!IsConnected) return;
await _mareHub!.SendAsync(nameof(UserAddPair), dto).ConfigureAwait(false);
}
public async Task UserRemovePair(UserDto dto)
{
if (!IsConnected) return;
await _mareHub!.SendAsync(nameof(UserRemovePair), dto).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,341 @@
using Dalamud.Utility;
using LZ4;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Files;
using MareSynchronos.API.Routes;
using MareSynchronos.FileCache;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI.Files.Models;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http.Json;
namespace MareSynchronos.WebAPI.Files;
public partial class FileDownloadManager : DisposableMediatorSubscriberBase
{
private readonly ConcurrentDictionary<Guid, bool> _downloadReady = new();
private readonly Dictionary<string, FileDownloadStatus> _downloadStatus;
private readonly FileCacheManager _fileDbManager;
private readonly FileTransferOrchestrator _orchestrator;
public FileDownloadManager(ILogger<FileDownloadManager> logger, MareMediator mediator,
FileTransferOrchestrator orchestrator,
FileCacheManager fileCacheManager) : base(logger, mediator)
{
_downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
_orchestrator = orchestrator;
_fileDbManager = fileCacheManager;
Mediator.Subscribe<DownloadReadyMessage>(this, (msg) =>
{
if (_downloadReady.ContainsKey(msg.RequestId))
{
_downloadReady[msg.RequestId] = true;
}
});
}
public List<DownloadFileTransfer> CurrentDownloads { get; private set; } = new();
public List<FileTransfer> ForbiddenTransfers => _orchestrator.ForbiddenTransfers;
public bool IsDownloading => !CurrentDownloads.Any();
public void CancelDownload()
{
CurrentDownloads.Clear();
_downloadStatus.Clear();
}
public async Task DownloadFiles(GameObjectHandler gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct)
{
Mediator.Publish(new HaltScanMessage("Download"));
try
{
await DownloadFilesInternal(gameObject, fileReplacementDto, ct).ConfigureAwait(false);
}
catch
{
CancelDownload();
}
finally
{
Mediator.Publish(new DownloadFinishedMessage(gameObject));
Mediator.Publish(new ResumeScanMessage("Download"));
}
}
protected override void Dispose(bool disposing)
{
CancelDownload();
base.Dispose(disposing);
}
private async Task DownloadFileHttpClient(string downloadGroup, DownloadFileTransfer fileTransfer, string tempPath, IProgress<long> progress, CancellationToken ct)
{
var requestId = await GetQueueRequest(fileTransfer, ct).ConfigureAwait(false);
Logger.LogDebug("GUID {requestId} for file {hash} on server {uri}", requestId, fileTransfer.Hash, fileTransfer.DownloadUri);
await WaitForDownloadReady(fileTransfer, requestId, ct).ConfigureAwait(false);
_downloadStatus[downloadGroup].DownloadStatus = DownloadStatus.Downloading;
HttpResponseMessage response = null!;
var requestUrl = MareFiles.CacheGetFullPath(fileTransfer.DownloadUri, requestId);
Logger.LogDebug("Downloading {requestUrl} for file {hash}", requestUrl, fileTransfer.Hash);
try
{
response = await _orchestrator.SendRequestAsync(HttpMethod.Get, requestUrl, ct).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
catch (HttpRequestException ex)
{
Logger.LogWarning(ex, "Error during download of {requestUrl}, HttpStatusCode: {code}", requestUrl, ex.StatusCode);
if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized)
{
throw new InvalidDataException($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex);
}
}
try
{
var fileStream = File.Create(tempPath);
await using (fileStream.ConfigureAwait(false))
{
var bufferSize = response.Content.Headers.ContentLength > 1024 * 1024 ? 4096 : 1024;
var buffer = new byte[bufferSize];
var bytesRead = 0;
while ((bytesRead = await (await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false)).ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
{
ct.ThrowIfCancellationRequested();
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), ct).ConfigureAwait(false);
progress.Report(bytesRead);
}
Logger.LogDebug("{requestUrl} downloaded to {tempPath}", requestUrl, tempPath);
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during file download of {requestUrl}", requestUrl);
try
{
if (!tempPath.IsNullOrEmpty())
File.Delete(tempPath);
}
catch
{
// ignore if file deletion fails
}
throw;
}
}
private async Task DownloadFilesInternal(GameObjectHandler gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct)
{
Logger.LogDebug("Downloading files for {id}", gameObjectHandler.Name);
// force create lazy
_ = gameObjectHandler.GameObjectLazy.Value;
List<DownloadFileDto> downloadFileInfoFromService = new();
downloadFileInfoFromService.AddRange(await FilesGetSizes(fileReplacement.Select(f => f.Hash).ToList(), ct).ConfigureAwait(false));
Logger.LogDebug("Files with size 0 or less: {files}", string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash)));
CurrentDownloads = downloadFileInfoFromService.Distinct().Select(d => new DownloadFileTransfer(d))
.Where(d => d.CanBeTransferred).ToList();
foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden))
{
if (!_orchestrator.ForbiddenTransfers.Any(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal)))
{
_orchestrator.ForbiddenTransfers.Add(new DownloadFileTransfer(dto));
}
}
var downloadGroups = CurrentDownloads.Where(f => f.CanBeTransferred).GroupBy(f => f.DownloadUri.Host + ":" + f.DownloadUri.Port, StringComparer.Ordinal);
foreach (var downloadGroup in downloadGroups)
{
_downloadStatus[downloadGroup.Key] = new FileDownloadStatus()
{
DownloadStatus = DownloadStatus.Initializing,
TotalBytes = downloadGroup.Sum(c => c.Total),
TotalFiles = downloadGroup.Count(),
TransferredBytes = 0,
TransferredFiles = 0
};
}
Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus));
await Parallel.ForEachAsync(downloadGroups, new ParallelOptions()
{
MaxDegreeOfParallelism = downloadGroups.Count(),
CancellationToken = ct,
},
async (fileGroup, token) =>
{
// let server predownload files
await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.RequestEnqueueFullPath(fileGroup.First().DownloadUri),
fileGroup.Select(c => c.Hash), token).ConfigureAwait(false);
foreach (var file in fileGroup)
{
var tempPath = _fileDbManager.GetCacheFilePath(file.Hash, isTemporaryFile: true);
Progress<long> progress = new((bytesDownloaded) =>
{
if (!_downloadStatus.ContainsKey(fileGroup.Key)) return;
_downloadStatus[fileGroup.Key].TransferredBytes += bytesDownloaded;
file.Transferred += bytesDownloaded;
});
try
{
_downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForSlot;
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
_downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForQueue;
await DownloadFileHttpClient(fileGroup.Key, file, tempPath, progress, token).ConfigureAwait(false);
_downloadStatus[fileGroup.Key].TransferredFiles += 1;
}
catch (OperationCanceledException)
{
File.Delete(tempPath);
Logger.LogDebug("Detected cancellation, removing {id}", gameObjectHandler);
CancelDownload();
return;
}
catch (Exception ex)
{
Logger.LogError(ex, "Error during download of {hash}", file.Hash);
continue;
}
finally
{
_orchestrator.ReleaseDownloadSlot();
}
_downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.Decompressing;
var tempFileData = await File.ReadAllBytesAsync(tempPath, token).ConfigureAwait(false);
var extractedFile = LZ4Codec.Unwrap(tempFileData);
File.Delete(tempPath);
var filePath = _fileDbManager.GetCacheFilePath(file.Hash, isTemporaryFile: false);
await File.WriteAllBytesAsync(filePath, extractedFile, token).ConfigureAwait(false);
var fi = new FileInfo(filePath);
Func<DateTime> RandomDayInThePast()
{
DateTime start = new(1995, 1, 1);
Random gen = new();
int range = (DateTime.Today - start).Days;
return () => start.AddDays(gen.Next(range));
}
fi.CreationTime = RandomDayInThePast().Invoke();
fi.LastAccessTime = DateTime.Today;
fi.LastWriteTime = RandomDayInThePast().Invoke();
try
{
_ = _fileDbManager.CreateCacheEntry(filePath);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Issue creating cache entry");
}
}
}).ConfigureAwait(false);
Logger.LogDebug("Download for {id} complete", gameObjectHandler);
CancelDownload();
}
private async Task<List<DownloadFileDto>> FilesGetSizes(List<string> hashes, CancellationToken ct)
{
if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized");
var response = await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.ServerFilesGetSizesFullPath(_orchestrator.FilesCdnUri!), hashes, ct).ConfigureAwait(false);
return await response.Content.ReadFromJsonAsync<List<DownloadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? new List<DownloadFileDto>();
}
private async Task<Guid> GetQueueRequest(DownloadFileTransfer downloadFileTransfer, CancellationToken ct)
{
var response = await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestRequestFileFullPath(downloadFileTransfer.DownloadUri, downloadFileTransfer.Hash), ct).ConfigureAwait(false);
var responseString = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
var requestId = Guid.Parse(responseString.Trim('"'));
if (!_downloadReady.ContainsKey(requestId))
{
_downloadReady[requestId] = false;
}
return requestId;
}
private async Task WaitForDownloadReady(DownloadFileTransfer downloadFileTransfer, Guid requestId, CancellationToken downloadCt)
{
bool alreadyCancelled = false;
try
{
CancellationTokenSource localTimeoutCts = new();
localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5));
CancellationTokenSource composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token);
while (_downloadReady.TryGetValue(requestId, out bool isReady) && !isReady)
{
try
{
await Task.Delay(250, composite.Token).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
if (downloadCt.IsCancellationRequested) throw;
var req = await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestCheckQueueFullPath(downloadFileTransfer.DownloadUri, requestId, downloadFileTransfer.Hash), downloadCt).ConfigureAwait(false);
req.EnsureSuccessStatusCode();
localTimeoutCts.Dispose();
composite.Dispose();
localTimeoutCts = new();
localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5));
composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token);
}
}
localTimeoutCts.Dispose();
composite.Dispose();
Logger.LogDebug("Download {requestId} ready", requestId);
}
catch (TaskCanceledException)
{
try
{
await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestCancelFullPath(downloadFileTransfer.DownloadUri, requestId)).ConfigureAwait(false);
alreadyCancelled = true;
}
catch
{
// ignore whatever happens here
}
throw;
}
finally
{
if (downloadCt.IsCancellationRequested && !alreadyCancelled)
{
try
{
await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestCancelFullPath(downloadFileTransfer.DownloadUri, requestId)).ConfigureAwait(false);
}
catch
{
// ignore whatever happens here
}
}
_downloadReady.Remove(requestId, out _);
}
}
}

View File

@@ -0,0 +1,109 @@
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.WebAPI.Files.Models;
using Microsoft.Extensions.Logging;
using System.Net.Http.Headers;
using System.Net.Http.Json;
namespace MareSynchronos.WebAPI.Files;
public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
{
private readonly HttpClient _httpClient;
private readonly MareConfigService _mareConfig;
private readonly object _semaphoreModificationLock = new();
private readonly ServerConfigurationManager _serverManager;
private int _availableDownloadSlots;
private SemaphoreSlim _downloadSemaphore;
public FileTransferOrchestrator(ILogger<FileTransferOrchestrator> logger, MareConfigService mareConfig, ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator)
{
_mareConfig = mareConfig;
_serverManager = serverManager;
_httpClient = new();
_availableDownloadSlots = mareConfig.Current.ParallelDownloads;
_downloadSemaphore = new(_availableDownloadSlots);
Mediator.Subscribe<ConnectedMessage>(this, (msg) =>
{
FilesCdnUri = msg.Connection.ServerInfo.FileServerAddress;
});
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
{
FilesCdnUri = null;
});
}
public Uri? FilesCdnUri { private set; get; }
public List<FileTransfer> ForbiddenTransfers { get; } = new();
public bool IsInitialized => FilesCdnUri != null;
public void ReleaseDownloadSlot()
{
_downloadSemaphore.Release();
}
public async Task<HttpResponseMessage> SendRequestAsync(HttpMethod method, Uri uri, CancellationToken? ct = null)
{
using var requestMessage = new HttpRequestMessage(method, uri);
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
}
public async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct) where T : class
{
using var requestMessage = new HttpRequestMessage(method, uri);
requestMessage.Content = JsonContent.Create(content);
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
}
public async Task<HttpResponseMessage> SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content, CancellationToken ct)
{
using var requestMessage = new HttpRequestMessage(method, uri);
requestMessage.Content = content;
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
}
public async Task WaitForDownloadSlotAsync(CancellationToken token)
{
lock (_semaphoreModificationLock)
{
if (_availableDownloadSlots != _mareConfig.Current.ParallelDownloads && _availableDownloadSlots == _downloadSemaphore.CurrentCount)
{
_availableDownloadSlots = _mareConfig.Current.ParallelDownloads;
_downloadSemaphore = new(_availableDownloadSlots);
}
}
await _downloadSemaphore.WaitAsync(token).ConfigureAwait(false);
}
private async Task<HttpResponseMessage> SendRequestInternalAsync(HttpRequestMessage requestMessage, CancellationToken? ct = null)
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _serverManager.GetToken());
if (requestMessage.Content != null && requestMessage.Content is not StreamContent)
{
var content = await ((JsonContent)requestMessage.Content).ReadAsStringAsync().ConfigureAwait(false);
Logger.LogDebug("Sending {method} to {uri} (Content: {content})", requestMessage.Method, requestMessage.RequestUri, content);
}
else
{
Logger.LogDebug("Sending {method} to {uri}", requestMessage.Method, requestMessage.RequestUri);
}
try
{
if (ct != null)
return await _httpClient.SendAsync(requestMessage, ct.Value).ConfigureAwait(false);
return await _httpClient.SendAsync(requestMessage).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogCritical(ex, "Error during SendRequestInternal for {uri}", requestMessage.RequestUri);
throw;
}
}
}

View File

@@ -0,0 +1,226 @@
using LZ4;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Files;
using MareSynchronos.API.Routes;
using MareSynchronos.FileCache;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI;
using MareSynchronos.WebAPI.Files.Models;
using Microsoft.Extensions.Logging;
using System.Net.Http.Headers;
using System.Net.Http.Json;
namespace MareSynchronos.WebAPI.Files;
public sealed class FileUploadManager : DisposableMediatorSubscriberBase
{
private readonly FileCacheManager _fileDbManager;
private readonly FileTransferOrchestrator _orchestrator;
private readonly ServerConfigurationManager _serverManager;
private readonly Dictionary<string, DateTime> _verifiedUploadedHashes = new(StringComparer.Ordinal);
private CancellationTokenSource? _uploadCancellationTokenSource = new();
public FileUploadManager(ILogger<FileUploadManager> logger, MareMediator mediator,
FileTransferOrchestrator orchestrator,
FileCacheManager fileDbManager,
ServerConfigurationManager serverManager) : base(logger, mediator)
{
_orchestrator = orchestrator;
_fileDbManager = fileDbManager;
_serverManager = serverManager;
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
{
Reset();
});
}
public List<FileTransfer> CurrentUploads { get; } = new();
public bool IsUploading => CurrentUploads.Count > 0;
public bool CancelUpload()
{
if (CurrentUploads.Any())
{
Logger.LogDebug("Cancelling current upload");
_uploadCancellationTokenSource?.Cancel();
_uploadCancellationTokenSource?.Dispose();
_uploadCancellationTokenSource = null;
CurrentUploads.Clear();
return true;
}
return false;
}
public async Task DeleteAllFiles()
{
if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized");
await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.ServerFilesDeleteAllFullPath(_orchestrator.FilesCdnUri!)).ConfigureAwait(false);
}
public async Task<CharacterData> UploadFiles(CharacterData data, List<UserData> visiblePlayers)
{
CancelUpload();
_uploadCancellationTokenSource = new CancellationTokenSource();
var uploadToken = _uploadCancellationTokenSource.Token;
Logger.LogDebug("Sending Character data {hash} to service {url}", data.DataHash.Value, _serverManager.CurrentApiUrl);
HashSet<string> unverifiedUploads = GetUnverifiedFiles(data);
if (unverifiedUploads.Any())
{
await UploadUnverifiedFiles(unverifiedUploads, visiblePlayers, uploadToken).ConfigureAwait(false);
Logger.LogInformation("Upload complete for {hash}", data.DataHash.Value);
}
foreach (var kvp in data.FileReplacements)
{
data.FileReplacements[kvp.Key].RemoveAll(i => _orchestrator.ForbiddenTransfers.Any(f => string.Equals(f.Hash, i.Hash, StringComparison.OrdinalIgnoreCase)));
}
return data;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Reset();
}
private async Task<List<UploadFileDto>> FilesSend(List<string> hashes, List<string> uids, CancellationToken ct)
{
if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized");
FilesSendDto filesSendDto = new()
{
FileHashes = hashes,
UIDs = uids
};
var response = await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.ServerFilesFilesSendFullPath(_orchestrator.FilesCdnUri!), filesSendDto, ct).ConfigureAwait(false);
return await response.Content.ReadFromJsonAsync<List<UploadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? new List<UploadFileDto>();
}
private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken)
{
var fileCache = _fileDbManager.GetFileCacheByHash(fileHash)!.ResolvedFilepath;
return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0,
(int)new FileInfo(fileCache).Length));
}
private HashSet<string> GetUnverifiedFiles(CharacterData data)
{
HashSet<string> unverifiedUploadHashes = new(StringComparer.Ordinal);
foreach (var item in data.FileReplacements.SelectMany(c => c.Value.Where(f => string.IsNullOrEmpty(f.FileSwapPath)).Select(v => v.Hash).Distinct(StringComparer.Ordinal)).Distinct(StringComparer.Ordinal).ToList())
{
if (!_verifiedUploadedHashes.TryGetValue(item, out var verifiedTime))
{
verifiedTime = DateTime.MinValue;
}
if (verifiedTime < DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(10)))
{
Logger.LogTrace("Verifying {item}, last verified: {date}", item, verifiedTime);
unverifiedUploadHashes.Add(item);
}
}
return unverifiedUploadHashes;
}
private void Reset()
{
_uploadCancellationTokenSource?.Cancel();
_uploadCancellationTokenSource?.Dispose();
_uploadCancellationTokenSource = null;
CurrentUploads.Clear();
_verifiedUploadedHashes.Clear();
}
private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken)
{
if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized");
Logger.LogInformation("Uploading {file}, {size}", fileHash, UiSharedService.ByteToString(compressedFile.Length));
if (uploadToken.IsCancellationRequested) return;
using var ms = new MemoryStream(compressedFile);
Progress<UploadProgress> prog = new((prog) =>
{
CurrentUploads.Single(f => string.Equals(f.Hash, fileHash, StringComparison.Ordinal)).Transferred = prog.Uploaded;
});
var streamContent = new ProgressableStreamContent(ms, prog);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
var response = await _orchestrator.SendRequestStreamAsync(HttpMethod.Post, MareFiles.ServerFilesUploadFullPath(_orchestrator.FilesCdnUri!, fileHash), streamContent, uploadToken).ConfigureAwait(false);
Logger.LogDebug("Upload Status: {status}", response.StatusCode);
}
private async Task UploadUnverifiedFiles(HashSet<string> unverifiedUploadHashes, List<UserData> visiblePlayers, CancellationToken uploadToken)
{
unverifiedUploadHashes = unverifiedUploadHashes.Where(h => _fileDbManager.GetFileCacheByHash(h) != null).ToHashSet(StringComparer.Ordinal);
Logger.LogDebug("Verifying {count} files", unverifiedUploadHashes.Count);
var filesToUpload = await FilesSend(unverifiedUploadHashes.ToList(), visiblePlayers.Select(p => p.UID).ToList(), uploadToken).ConfigureAwait(false);
foreach (var file in filesToUpload.Where(f => !f.IsForbidden))
{
try
{
CurrentUploads.Add(new UploadFileTransfer(file)
{
Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length,
});
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Tried to request file {hash} but file was not present", file.Hash);
}
}
foreach (var file in filesToUpload.Where(c => c.IsForbidden))
{
if (_orchestrator.ForbiddenTransfers.All(f => !string.Equals(f.Hash, file.Hash, StringComparison.Ordinal)))
{
_orchestrator.ForbiddenTransfers.Add(new UploadFileTransfer(file)
{
LocalFile = _fileDbManager.GetFileCacheByHash(file.Hash)?.ResolvedFilepath ?? string.Empty,
});
}
_verifiedUploadedHashes[file.Hash] = DateTime.UtcNow;
}
var totalSize = CurrentUploads.Sum(c => c.Total);
Logger.LogDebug("Compressing and uploading files");
Task uploadTask = Task.CompletedTask;
foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList())
{
Logger.LogDebug("Compressing {file}", file);
var data = await GetCompressedFileData(file.Hash, uploadToken).ConfigureAwait(false);
CurrentUploads.Single(e => string.Equals(e.Hash, data.Item1, StringComparison.Ordinal)).Total = data.Item2.Length;
await uploadTask.ConfigureAwait(false);
uploadTask = UploadFile(data.Item2, file.Hash, uploadToken);
uploadToken.ThrowIfCancellationRequested();
_verifiedUploadedHashes[file.Hash] = DateTime.UtcNow;
}
if (CurrentUploads.Any())
{
await uploadTask.ConfigureAwait(false);
var compressedSize = CurrentUploads.Sum(c => c.Total);
Logger.LogDebug("Upload complete, compressed {size} to {compressed}", UiSharedService.ByteToString(totalSize), UiSharedService.ByteToString(compressedSize));
}
foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Any(u => string.Equals(u.Hash, c, StringComparison.Ordinal))))
{
_verifiedUploadedHashes[file] = DateTime.UtcNow;
}
CurrentUploads.Clear();
}
}

View File

@@ -1,6 +1,6 @@
using MareSynchronos.API.Dto.Files;
namespace MareSynchronos.WebAPI.Utils;
namespace MareSynchronos.WebAPI.Files.Models;
public class DownloadFileTransfer : FileTransfer
{
@@ -9,7 +9,10 @@ public class DownloadFileTransfer : FileTransfer
public Uri DownloadUri => new(Dto.Url);
public override long Total
{
set { }
set
{
// nothing to set
}
get => Dto.Size;
}

View File

@@ -0,0 +1,10 @@
namespace MareSynchronos.WebAPI.Files.Models;
public enum DownloadStatus
{
Initializing,
WaitingForSlot,
WaitingForQueue,
Downloading,
Decompressing
}

View File

@@ -0,0 +1,10 @@
namespace MareSynchronos.WebAPI.Files.Models;
public class FileDownloadStatus
{
public DownloadStatus DownloadStatus { get; set; }
public int TotalFiles { get; set; }
public int TransferredFiles { get; set; }
public long TotalBytes { get; set; }
public long TransferredBytes { get; set; }
}

View File

@@ -1,6 +1,6 @@
using MareSynchronos.API.Dto.Files;
namespace MareSynchronos.WebAPI.Utils;
namespace MareSynchronos.WebAPI.Files.Models;
public abstract class FileTransfer
{

View File

@@ -0,0 +1,93 @@
using System.Net;
namespace MareSynchronos.WebAPI.Files.Models;
public class ProgressableStreamContent : StreamContent
{
private const int _defaultBufferSize = 4096;
private readonly int _bufferSize;
private readonly IProgress<UploadProgress> _progress;
private readonly Stream _streamToWrite;
private bool _contentConsumed;
public ProgressableStreamContent(Stream streamToWrite, IProgress<UploadProgress> downloader)
: this(streamToWrite, _defaultBufferSize, downloader)
{
}
public ProgressableStreamContent(Stream streamToWrite, int bufferSize, IProgress<UploadProgress> progress)
: base(streamToWrite, bufferSize)
{
if (streamToWrite == null)
{
throw new ArgumentNullException(nameof(streamToWrite));
}
if (bufferSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(bufferSize));
}
_streamToWrite = streamToWrite;
_bufferSize = bufferSize;
_progress = progress;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_streamToWrite.Dispose();
}
base.Dispose(disposing);
}
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
{
PrepareContent();
var buffer = new byte[_bufferSize];
var size = _streamToWrite.Length;
var uploaded = 0;
using (_streamToWrite)
{
while (true)
{
var length = _streamToWrite.Read(buffer, 0, buffer.Length);
if (length <= 0)
{
break;
}
uploaded += length;
_progress.Report(new UploadProgress(uploaded, size));
await stream.WriteAsync(buffer.AsMemory(0, length)).ConfigureAwait(false);
}
}
}
protected override bool TryComputeLength(out long length)
{
length = _streamToWrite.Length;
return true;
}
private void PrepareContent()
{
if (_contentConsumed)
{
if (_streamToWrite.CanSeek)
{
_streamToWrite.Position = 0;
}
else
{
throw new InvalidOperationException("The stream has already been read.");
}
}
_contentConsumed = true;
}
}

View File

@@ -1,6 +1,6 @@
using MareSynchronos.API.Dto.Files;
namespace MareSynchronos.WebAPI.Utils;
namespace MareSynchronos.WebAPI.Files.Models;
public class UploadFileTransfer : FileTransfer
{

View File

@@ -0,0 +1,3 @@
namespace MareSynchronos.WebAPI.Files.Models;
public record UploadProgress(long Uploaded, long Size);

View File

@@ -0,0 +1,90 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.User;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
using System.Text;
namespace MareSynchronos.WebAPI;
public partial class ApiController
{
public async Task PushCharacterData(CharacterData data, List<UserData> visibleCharacters)
{
if (!IsConnected) return;
try
{
await PushCharacterDataInternal(data, visibleCharacters.ToList()).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
Logger.LogDebug("Upload operation was cancelled");
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during upload of files");
}
}
public async Task UserAddPair(UserDto user)
{
if (!IsConnected) return;
await _mareHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false);
}
public async Task UserDelete()
{
CheckConnection();
await _mareHub!.SendAsync(nameof(UserDelete)).ConfigureAwait(false);
await CreateConnections().ConfigureAwait(false);
}
public async Task<List<OnlineUserIdentDto>> UserGetOnlinePairs()
{
return await _mareHub!.InvokeAsync<List<OnlineUserIdentDto>>(nameof(UserGetOnlinePairs)).ConfigureAwait(false);
}
public async Task<List<UserPairDto>> UserGetPairedClients()
{
return await _mareHub!.InvokeAsync<List<UserPairDto>>(nameof(UserGetPairedClients)).ConfigureAwait(false);
}
public async Task UserPushData(UserCharaDataMessageDto dto)
{
try
{
await _mareHub!.InvokeAsync(nameof(UserPushData), dto).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to Push character data");
}
}
public async Task UserRemovePair(UserDto userDto)
{
if (!IsConnected) return;
await _mareHub!.SendAsync(nameof(UserRemovePair), userDto).ConfigureAwait(false);
}
public async Task UserSetPairPermissions(UserPermissionsDto userPermissions)
{
await _mareHub!.SendAsync(nameof(UserSetPairPermissions), userPermissions).ConfigureAwait(false);
}
private async Task PushCharacterDataInternal(CharacterData character, List<UserData> visibleCharacters)
{
Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID)));
StringBuilder sb = new();
foreach (var kvp in character.FileReplacements.ToList())
{
sb.AppendLine($"FileReplacements for {kvp.Key}: {kvp.Value.Count}");
}
foreach (var item in character.GlamourerData)
{
sb.AppendLine($"GlamourerData for {item.Key}: {!string.IsNullOrEmpty(item.Value)}");
}
Logger.LogDebug("Chara data contained: {nl} {data}", Environment.NewLine, sb.ToString());
await UserPushData(new(visibleCharacters, character)).ConfigureAwait(false);
}
}

View File

@@ -3,7 +3,7 @@ using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Dto;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.API.Dto.User;
using MareSynchronos.Mediator;
using MareSynchronos.Services.Mediator;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
@@ -11,255 +11,89 @@ namespace MareSynchronos.WebAPI;
public partial class ApiController
{
private void ExecuteSafely(Action act)
public Task Client_DownloadReady(Guid requestId)
{
try
{
act();
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Error on executing safely");
}
}
public void OnUpdateSystemInfo(Action<SystemInfoDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UpdateSystemInfo), act);
}
public void OnUserReceiveCharacterData(Action<OnlineUserCharaDataDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserReceiveCharacterData), act);
}
public void OnReceiveServerMessage(Action<MessageSeverity, string> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_ReceiveServerMessage), act);
}
public void OnDownloadReady(Action<Guid> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_DownloadReady), act);
}
public void OnGroupSendFullInfo(Action<GroupFullInfoDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupSendFullInfo), act);
}
public Task Client_GroupSendFullInfo(GroupFullInfoDto dto)
{
_logger.LogTrace("Client_GroupSendFullInfo: " + dto);
ExecuteSafely(() => _pairManager.AddGroup(dto));
Logger.LogDebug("Server sent {requestId} ready", requestId);
Mediator.Publish(new DownloadReadyMessage(requestId));
return Task.CompletedTask;
}
public void OnGroupSendInfo(Action<GroupInfoDto> act)
public Task Client_GroupChangePermissions(GroupPermissionDto groupPermission)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupSendInfo), act);
}
public Task Client_GroupSendInfo(GroupInfoDto dto)
{
_logger.LogTrace("Client_GroupSendInfo: " + dto);
ExecuteSafely(() => _pairManager.SetGroupInfo(dto));
Logger.LogTrace("Client_GroupChangePermissions: {perm}", groupPermission);
ExecuteSafely(() => _pairManager.SetGroupPermissions(groupPermission));
return Task.CompletedTask;
}
public void OnGroupDelete(Action<GroupDto> act)
public Task Client_GroupDelete(GroupDto groupDto)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupDelete), act);
}
public Task Client_GroupDelete(GroupDto dto)
{
_logger.LogTrace("Client_GroupDelete: " + dto);
ExecuteSafely(() => _pairManager.RemoveGroup(dto.Group));
Logger.LogTrace("Client_GroupDelete: {dto}", groupDto);
ExecuteSafely(() => _pairManager.RemoveGroup(groupDto.Group));
return Task.CompletedTask;
}
public void OnGroupPairJoined(Action<GroupPairFullInfoDto> act)
public Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto permissionDto)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupPairJoined), act);
}
public Task Client_GroupPairJoined(GroupPairFullInfoDto dto)
{
_logger.LogTrace("Client_GroupPairJoined: " + dto);
ExecuteSafely(() => _pairManager.AddGroupPair(dto));
return Task.CompletedTask;
}
public void OnGroupPairLeft(Action<GroupPairDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupPairLeft), act);
}
public Task Client_GroupPairLeft(GroupPairDto dto)
{
_logger.LogTrace("Client_GroupPairLeft: " + dto);
ExecuteSafely(() => _pairManager.RemoveGroupPair(dto));
return Task.CompletedTask;
}
public void OnGroupChangePermissions(Action<GroupPermissionDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupChangePermissions), act);
}
public Task Client_GroupChangePermissions(GroupPermissionDto dto)
{
_logger.LogTrace("Client_GroupChangePermissions: " + dto);
ExecuteSafely(() => _pairManager.SetGroupPermissions(dto));
return Task.CompletedTask;
}
public void OnGroupPairChangePermissions(Action<GroupPairUserPermissionDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupPairChangePermissions), act);
}
public Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto dto)
{
_logger.LogTrace("Client_GroupPairChangePermissions: " + dto);
Logger.LogTrace("Client_GroupPairChangePermissions: {perm}", permissionDto);
ExecuteSafely(() =>
{
if (string.Equals(dto.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupUserPermissions(dto);
else _pairManager.SetGroupPairUserPermissions(dto);
if (string.Equals(permissionDto.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupUserPermissions(permissionDto);
else _pairManager.SetGroupPairUserPermissions(permissionDto);
});
return Task.CompletedTask;
}
public void OnGroupPairChangeUserInfo(Action<GroupPairUserInfoDto> act)
public Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto userInfo)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupPairChangeUserInfo), act);
}
public Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto dto)
{
_logger.LogTrace("Client_GroupPairChangeUserInfo: " + dto);
Logger.LogTrace("Client_GroupPairChangeUserInfo: {dto}", userInfo);
ExecuteSafely(() =>
{
if (string.Equals(dto.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupStatusInfo(dto);
else _pairManager.SetGroupPairStatusInfo(dto);
if (string.Equals(userInfo.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupStatusInfo(userInfo);
else _pairManager.SetGroupPairStatusInfo(userInfo);
});
return Task.CompletedTask;
}
public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dto)
public Task Client_GroupPairJoined(GroupPairFullInfoDto groupPairInfoDto)
{
_logger.LogTrace("Client_UserReceiveCharacterData: " + dto.User);
ExecuteSafely(() => _pairManager.ReceiveCharaData(dto));
Logger.LogTrace("Client_GroupPairJoined: {dto}", groupPairInfoDto);
ExecuteSafely(() => _pairManager.AddGroupPair(groupPairInfoDto));
return Task.CompletedTask;
}
public void OnUserAddClientPair(Action<UserPairDto> act)
public Task Client_GroupPairLeft(GroupPairDto groupPairDto)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserAddClientPair), act);
}
public Task Client_UserAddClientPair(UserPairDto dto)
{
_logger.LogDebug($"Client_UserAddClientPair: " + dto);
ExecuteSafely(() => _pairManager.AddUserPair(dto));
Logger.LogTrace("Client_GroupPairLeft: {dto}", groupPairDto);
ExecuteSafely(() => _pairManager.RemoveGroupPair(groupPairDto));
return Task.CompletedTask;
}
public void OnUserRemoveClientPair(Action<UserDto> act)
public Task Client_GroupSendFullInfo(GroupFullInfoDto groupInfo)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserRemoveClientPair), act);
}
public Task Client_UserRemoveClientPair(UserDto dto)
{
_logger.LogDebug($"Client_UserRemoveClientPair: " + dto);
ExecuteSafely(() => _pairManager.RemoveUserPair(dto));
Logger.LogTrace("Client_GroupSendFullInfo: {dto}", groupInfo);
ExecuteSafely(() => _pairManager.AddGroup(groupInfo));
return Task.CompletedTask;
}
public void OnUserSendOffline(Action<UserDto> act)
public Task Client_GroupSendInfo(GroupInfoDto groupInfo)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserSendOffline), act);
}
public Task Client_UserSendOffline(UserDto dto)
{
_logger.LogDebug($"Client_UserSendOffline: {dto}");
ExecuteSafely(() => _pairManager.MarkPairOffline(dto.User));
Logger.LogTrace("Client_GroupSendInfo: {dto}", groupInfo);
ExecuteSafely(() => _pairManager.SetGroupInfo(groupInfo));
return Task.CompletedTask;
}
public void OnUserSendOnline(Action<OnlineUserIdentDto> act)
public Task Client_ReceiveServerMessage(MessageSeverity messageSeverity, string message)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserSendOnline), act);
}
public Task Client_UserSendOnline(OnlineUserIdentDto dto)
{
_logger.LogDebug($"Client_UserSendOnline: {dto}");
ExecuteSafely(() => _pairManager.MarkPairOnline(dto, this));
return Task.CompletedTask;
}
public void OnUserUpdateOtherPairPermissions(Action<UserPermissionsDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserUpdateOtherPairPermissions), act);
}
public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto)
{
_logger.LogDebug($"Client_UserUpdateOtherPairPermissions: {dto}");
ExecuteSafely(() => _pairManager.UpdatePairPermissions(dto));
return Task.CompletedTask;
}
public void OnUserUpdateSelfPairPermissions(Action<UserPermissionsDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserUpdateSelfPairPermissions), act);
}
public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto)
{
_logger.LogDebug($"Client_UserUpdateSelfPairPermissions: {dto}");
ExecuteSafely(() => _pairManager.UpdateSelfPairPermissions(dto));
return Task.CompletedTask;
}
public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo)
{
SystemInfoDto = systemInfo;
return Task.CompletedTask;
}
public Task Client_ReceiveServerMessage(MessageSeverity severity, string message)
{
switch (severity)
switch (messageSeverity)
{
case MessageSeverity.Error:
Mediator.Publish(new NotificationMessage("Warning from " + _serverManager.CurrentServer!.ServerName, message, NotificationType.Error, 7500));
break;
case MessageSeverity.Warning:
Mediator.Publish(new NotificationMessage("Warning from " + _serverManager.CurrentServer!.ServerName, message, NotificationType.Warning, 7500));
break;
case MessageSeverity.Information:
if (_doNotNotifyOnNextInfo)
{
@@ -273,10 +107,191 @@ public partial class ApiController
return Task.CompletedTask;
}
public Task Client_DownloadReady(Guid requestId)
public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo)
{
_logger.LogDebug($"Server sent {requestId} ready");
_downloadReady[requestId] = true;
SystemInfoDto = systemInfo;
return Task.CompletedTask;
}
}
public Task Client_UserAddClientPair(UserPairDto dto)
{
Logger.LogDebug("Client_UserAddClientPair: {dto}", dto);
ExecuteSafely(() => _pairManager.AddUserPair(dto));
return Task.CompletedTask;
}
public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto)
{
Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User);
ExecuteSafely(() => _pairManager.ReceiveCharaData(dataDto));
return Task.CompletedTask;
}
public Task Client_UserReceiveUploadStatus(UserDto dto)
{
Logger.LogTrace("Client_UserReceiveUploadStatus: {dto}", dto);
ExecuteSafely(() => _pairManager.ReceiveUploadStatus(dto));
return Task.CompletedTask;
}
public Task Client_UserRemoveClientPair(UserDto dto)
{
Logger.LogDebug("Client_UserRemoveClientPair: {dto}", dto);
ExecuteSafely(() => _pairManager.RemoveUserPair(dto));
return Task.CompletedTask;
}
public Task Client_UserSendOffline(UserDto dto)
{
Logger.LogDebug("Client_UserSendOffline: {dto}", dto);
ExecuteSafely(() => _pairManager.MarkPairOffline(dto.User));
return Task.CompletedTask;
}
public Task Client_UserSendOnline(OnlineUserIdentDto dto)
{
Logger.LogDebug("Client_UserSendOnline: {dto}", dto);
ExecuteSafely(() => _pairManager.MarkPairOnline(dto));
return Task.CompletedTask;
}
public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto)
{
Logger.LogDebug("Client_UserUpdateOtherPairPermissions: {dto}", dto);
ExecuteSafely(() => _pairManager.UpdatePairPermissions(dto));
return Task.CompletedTask;
}
public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto)
{
Logger.LogDebug("Client_UserUpdateSelfPairPermissions: {dto}", dto);
ExecuteSafely(() => _pairManager.UpdateSelfPairPermissions(dto));
return Task.CompletedTask;
}
public void OnDownloadReady(Action<Guid> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_DownloadReady), act);
}
public void OnGroupChangePermissions(Action<GroupPermissionDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupChangePermissions), act);
}
public void OnGroupDelete(Action<GroupDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupDelete), act);
}
public void OnGroupPairChangePermissions(Action<GroupPairUserPermissionDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupPairChangePermissions), act);
}
public void OnGroupPairChangeUserInfo(Action<GroupPairUserInfoDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupPairChangeUserInfo), act);
}
public void OnGroupPairJoined(Action<GroupPairFullInfoDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupPairJoined), act);
}
public void OnGroupPairLeft(Action<GroupPairDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupPairLeft), act);
}
public void OnGroupSendFullInfo(Action<GroupFullInfoDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupSendFullInfo), act);
}
public void OnGroupSendInfo(Action<GroupInfoDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupSendInfo), act);
}
public void OnReceiveServerMessage(Action<MessageSeverity, string> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_ReceiveServerMessage), act);
}
public void OnUpdateSystemInfo(Action<SystemInfoDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UpdateSystemInfo), act);
}
public void OnUserAddClientPair(Action<UserPairDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserAddClientPair), act);
}
public void OnUserReceiveCharacterData(Action<OnlineUserCharaDataDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserReceiveCharacterData), act);
}
public void OnUserReceiveUploadStatus(Action<UserDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserReceiveUploadStatus), act);
}
public void OnUserRemoveClientPair(Action<UserDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserRemoveClientPair), act);
}
public void OnUserSendOffline(Action<UserDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserSendOffline), act);
}
public void OnUserSendOnline(Action<OnlineUserIdentDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserSendOnline), act);
}
public void OnUserUpdateOtherPairPermissions(Action<UserPermissionsDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserUpdateOtherPairPermissions), act);
}
public void OnUserUpdateSelfPairPermissions(Action<UserPermissionsDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserUpdateSelfPairPermissions), act);
}
private void ExecuteSafely(Action act)
{
try
{
act();
}
catch (Exception ex)
{
Logger.LogCritical(ex, "Error on executing safely");
}
}
}

View File

@@ -1,56 +1,11 @@
using MareSynchronos.API.Dto.Group;
using MareSynchronos.WebAPI.SignalR.Utils;
using Microsoft.AspNetCore.SignalR.Client;
namespace MareSynchronos.WebAPI;
public partial class ApiController
{
private void CheckConnection()
{
if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new System.Exception("Not connected");
}
public async Task<List<BannedGroupUserDto>> GroupGetBannedUsers(GroupDto group)
{
CheckConnection();
return await _mareHub!.InvokeAsync<List<BannedGroupUserDto>>(nameof(GroupGetBannedUsers), group).ConfigureAwait(false);
}
public async Task GroupClear(GroupDto group)
{
CheckConnection();
await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false);
}
public async Task GroupChangeOwnership(GroupPairDto groupPair)
{
CheckConnection();
await _mareHub!.SendAsync(nameof(GroupChangeOwnership), groupPair).ConfigureAwait(false);
}
public async Task<bool> GroupChangePassword(GroupPasswordDto groupPassword)
{
CheckConnection();
return await _mareHub!.InvokeAsync<bool>(nameof(GroupChangePassword), groupPassword).ConfigureAwait(false);
}
public async Task<GroupPasswordDto> GroupCreate()
{
CheckConnection();
return await _mareHub!.InvokeAsync<GroupPasswordDto>(nameof(GroupCreate)).ConfigureAwait(false);
}
public async Task<List<GroupFullInfoDto>> GroupsGetAll()
{
CheckConnection();
return await _mareHub!.InvokeAsync<List<GroupFullInfoDto>>(nameof(GroupsGetAll)).ConfigureAwait(false);
}
public async Task<List<GroupPairFullInfoDto>> GroupsGetUsersInGroup(GroupDto group)
{
CheckConnection();
return await _mareHub!.InvokeAsync<List<GroupPairFullInfoDto>>(nameof(GroupsGetUsersInGroup), group).ConfigureAwait(false);
}
public async Task GroupBanUser(GroupPairDto dto, string reason)
{
CheckConnection();
@@ -69,12 +24,48 @@ public partial class ApiController
await _mareHub!.SendAsync(nameof(GroupChangeIndividualPermissionState), dto).ConfigureAwait(false);
}
public async Task GroupChangeOwnership(GroupPairDto groupPair)
{
CheckConnection();
await _mareHub!.SendAsync(nameof(GroupChangeOwnership), groupPair).ConfigureAwait(false);
}
public async Task<bool> GroupChangePassword(GroupPasswordDto groupPassword)
{
CheckConnection();
return await _mareHub!.InvokeAsync<bool>(nameof(GroupChangePassword), groupPassword).ConfigureAwait(false);
}
public async Task GroupClear(GroupDto group)
{
CheckConnection();
await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false);
}
public async Task<GroupPasswordDto> GroupCreate()
{
CheckConnection();
return await _mareHub!.InvokeAsync<GroupPasswordDto>(nameof(GroupCreate)).ConfigureAwait(false);
}
public async Task<List<string>> GroupCreateTempInvite(GroupDto group, int amount)
{
CheckConnection();
return await _mareHub!.InvokeAsync<List<string>>(nameof(GroupCreateTempInvite), group, amount).ConfigureAwait(false);
}
public async Task GroupDelete(GroupDto group)
{
CheckConnection();
await _mareHub!.SendAsync(nameof(GroupDelete), group).ConfigureAwait(false);
}
public async Task<List<BannedGroupUserDto>> GroupGetBannedUsers(GroupDto group)
{
CheckConnection();
return await _mareHub!.InvokeAsync<List<BannedGroupUserDto>>(nameof(GroupGetBannedUsers), group).ConfigureAwait(false);
}
public async Task<bool> GroupJoin(GroupPasswordDto passwordedGroup)
{
CheckConnection();
@@ -93,21 +84,32 @@ public partial class ApiController
await _mareHub!.SendAsync(nameof(GroupRemoveUser), groupPair).ConfigureAwait(false);
}
public async Task GroupSetUserInfo(GroupPairUserInfoDto groupPair)
{
CheckConnection();
await _mareHub!.SendAsync(nameof(GroupSetUserInfo), groupPair).ConfigureAwait(false);
}
public async Task<List<GroupFullInfoDto>> GroupsGetAll()
{
CheckConnection();
return await _mareHub!.InvokeAsync<List<GroupFullInfoDto>>(nameof(GroupsGetAll)).ConfigureAwait(false);
}
public async Task<List<GroupPairFullInfoDto>> GroupsGetUsersInGroup(GroupDto group)
{
CheckConnection();
return await _mareHub!.InvokeAsync<List<GroupPairFullInfoDto>>(nameof(GroupsGetUsersInGroup), group).ConfigureAwait(false);
}
public async Task GroupUnbanUser(GroupPairDto groupPair)
{
CheckConnection();
await _mareHub!.SendAsync(nameof(GroupUnbanUser), groupPair).ConfigureAwait(false);
}
public async Task GroupSetUserInfo(GroupPairUserInfoDto userInfo)
private void CheckConnection()
{
CheckConnection();
await _mareHub!.SendAsync(nameof(GroupSetUserInfo), userInfo).ConfigureAwait(false);
}
public async Task<List<string>> GroupCreateTempInvite(GroupDto group, int amount)
{
CheckConnection();
return await _mareHub!.InvokeAsync<List<string>>(nameof(GroupCreateTempInvite), group, amount).ConfigureAwait(false);
if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new InvalidDataException("Not connected");
}
}

View File

@@ -1,68 +1,53 @@
using Dalamud.Interface.Internal.Notifications;
using System.Collections.Concurrent;
using MareSynchronos.API.Routes;
using MareSynchronos.FileCache;
using MareSynchronos.API.Routes;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI.Utils;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
using MareSynchronos.API.Dto;
using MareSynchronos.API.SignalR;
using MareSynchronos.Managers;
using Dalamud.Utility;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Mediator;
using MareSynchronos.Factories;
using System.Reflection;
using MareSynchronos.WebAPI.SignalR.Utils;
using MareSynchronos.WebAPI.SignalR;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.Services;
namespace MareSynchronos.WebAPI;
public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareHubClient
public sealed partial class ApiController : DisposableMediatorSubscriberBase, IMareHubClient
{
public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)";
public const string MainServiceUri = "wss://maresynchronos.com";
private readonly DalamudUtilService _dalamudUtil;
private readonly HubFactory _hubFactory;
private readonly MareConfigService _configService;
private readonly DalamudUtil _dalamudUtil;
private readonly FileCacheManager _fileDbManager;
private readonly PairManager _pairManager;
private readonly ServerConfigurationManager _serverManager;
private CancellationTokenSource _connectionCancellationTokenSource;
private HubConnection? _mareHub;
private CancellationTokenSource? _uploadCancellationTokenSource = new();
private CancellationTokenSource? _healthCheckTokenSource = new();
private bool _doNotNotifyOnNextInfo = false;
private ConnectionDto? _connectionDto;
public ServerInfo ServerInfo => _connectionDto?.ServerInfo ?? new ServerInfo();
public string AuthFailureMessage { get; private set; } = string.Empty;
private bool _doNotNotifyOnNextInfo = false;
private CancellationTokenSource? _healthCheckTokenSource = new();
private bool _initialized;
private HubConnection? _mareHub;
private ServerState _serverState;
public SystemInfoDto SystemInfoDto { get; private set; } = new();
private HttpClient _httpClient;
public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, MareConfigService configService, DalamudUtil dalamudUtil, FileCacheManager fileDbManager,
public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, DalamudUtilService dalamudUtil,
PairManager pairManager, ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator)
{
_logger.LogTrace("Creating " + nameof(ApiController));
_hubFactory = hubFactory;
_configService = configService;
_dalamudUtil = dalamudUtil;
_fileDbManager = fileDbManager;
_pairManager = pairManager;
_serverManager = serverManager;
_connectionCancellationTokenSource = new CancellationTokenSource();
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut());
Mediator.Subscribe<HubClosedMessage>(this, (msg) => MareHubOnClosed(((HubClosedMessage)msg).Exception));
Mediator.Subscribe<HubReconnectedMessage>(this, (msg) => MareHubOnReconnected(((HubReconnectedMessage)msg).Arg));
Mediator.Subscribe<HubReconnectingMessage>(this, (msg) => MareHubOnReconnecting(((HubReconnectingMessage)msg).Exception));
Mediator.Subscribe<HubClosedMessage>(this, (msg) => MareHubOnClosed(msg.Exception));
Mediator.Subscribe<HubReconnectedMessage>(this, (msg) => _ = Task.Run(MareHubOnReconnected));
Mediator.Subscribe<HubReconnectingMessage>(this, (msg) => MareHubOnReconnecting(msg.Exception));
ServerState = ServerState.Offline;
_verifiedUploadedHashes = new(StringComparer.Ordinal);
_httpClient = new();
if (_dalamudUtil.IsLoggedIn)
{
@@ -70,56 +55,40 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH
}
}
private void DalamudUtilOnLogOut()
{
Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false));
ServerState = ServerState.Offline;
}
private void DalamudUtilOnLogIn()
{
Task.Run(() => CreateConnections(forceGetToken: true));
}
public ConcurrentDictionary<int, List<DownloadFileTransfer>> CurrentDownloads { get; } = new();
public List<FileTransfer> CurrentUploads { get; } = new();
public List<FileTransfer> ForbiddenTransfers { get; } = new();
public bool IsConnected => ServerState == ServerState.Connected;
public bool IsDownloading => !CurrentDownloads.IsEmpty;
public bool IsUploading => CurrentUploads.Count > 0;
public bool ServerAlive => ServerState is ServerState.Connected or ServerState.RateLimited or ServerState.Unauthorized or ServerState.Disconnected;
public string UID => _connectionDto?.User.UID ?? string.Empty;
public string AuthFailureMessage { get; private set; } = string.Empty;
public Version CurrentClientVersion => _connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0);
public string DisplayName => _connectionDto?.User.AliasOrUID ?? string.Empty;
public bool IsConnected => ServerState == ServerState.Connected;
public bool IsCurrentVersion => (Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0, 0)) >= (_connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0, 0));
public int OnlineUsers => SystemInfoDto.OnlineUsers;
private ServerState _serverState;
private bool _initialized;
public bool ServerAlive => ServerState is ServerState.Connected or ServerState.RateLimited or ServerState.Unauthorized or ServerState.Disconnected;
public ServerInfo ServerInfo => _connectionDto?.ServerInfo ?? new ServerInfo();
public ServerState ServerState
{
get => _serverState;
private set
{
_logger.LogDebug("New ServerState: {value}, prev ServerState: {_serverState}", value, _serverState);
Logger.LogDebug("New ServerState: {value}, prev ServerState: {_serverState}", value, _serverState);
_serverState = value;
}
}
public SystemInfoDto SystemInfoDto { get; private set; } = new();
public string UID => _connectionDto?.User.UID ?? string.Empty;
public async Task<bool> CheckClientHealth()
{
return await _mareHub!.InvokeAsync<bool>(nameof(CheckClientHealth)).ConfigureAwait(false);
}
public async Task CreateConnections(bool forceGetToken = false)
{
_logger.LogDebug("CreateConnections called");
_httpClient?.Dispose();
_httpClient = new();
Logger.LogDebug("CreateConnections called");
if (_serverManager.CurrentServer?.FullPause ?? true)
{
_logger.LogInformation("Not recreating Connection, paused");
Logger.LogInformation("Not recreating Connection, paused");
_connectionDto = null;
await StopConnection(ServerState.Disconnected).ConfigureAwait(false);
_connectionCancellationTokenSource.Cancel();
@@ -129,7 +98,7 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH
var secretKey = _serverManager.GetSecretKey();
if (secretKey.IsNullOrEmpty())
{
_logger.LogWarning("No secret key set for current character");
Logger.LogWarning("No secret key set for current character");
_connectionDto = null;
await StopConnection(ServerState.NoSecretKey).ConfigureAwait(false);
_connectionCancellationTokenSource.Cancel();
@@ -138,12 +107,11 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH
await StopConnection(ServerState.Disconnected).ConfigureAwait(false);
_logger.LogInformation("Recreating Connection");
Logger.LogInformation("Recreating Connection");
_connectionCancellationTokenSource.Cancel();
_connectionCancellationTokenSource = new CancellationTokenSource();
var token = _connectionCancellationTokenSource.Token;
_verifiedUploadedHashes.Clear();
while (ServerState is not ServerState.Connected && !token.IsCancellationRequested)
{
AuthFailureMessage = string.Empty;
@@ -153,11 +121,11 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH
try
{
_logger.LogDebug("Building connection");
Logger.LogDebug("Building connection");
if (_serverManager.GetToken() == null || forceGetToken)
{
_logger.LogDebug("Requesting new JWT");
Logger.LogDebug("Requesting new JWT");
using HttpClient httpClient = new();
var postUri = MareAuth.AuthFullPath(new Uri(_serverManager.CurrentApiUrl
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
@@ -171,12 +139,12 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH
AuthFailureMessage = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
result.EnsureSuccessStatusCode();
_serverManager.SaveToken(await result.Content.ReadAsStringAsync().ConfigureAwait(false));
_logger.LogDebug("JWT Success");
Logger.LogDebug("JWT Success");
}
while (!_dalamudUtil.IsPlayerPresent && !token.IsCancellationRequested)
{
_logger.LogDebug("Player not loaded in yet, waiting");
Logger.LogDebug("Player not loaded in yet, waiting");
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
}
@@ -200,7 +168,7 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH
}
catch (HttpRequestException ex)
{
_logger.LogWarning(ex, "HttpRequestException on Connection");
Logger.LogWarning(ex, "HttpRequestException on Connection");
if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
@@ -209,34 +177,61 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH
}
ServerState = ServerState.Reconnecting;
_logger.LogInformation("Failed to establish connection, retrying");
Logger.LogInformation("Failed to establish connection, retrying");
await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Exception on Connection");
Logger.LogWarning(ex, "Exception on Connection");
_logger.LogInformation("Failed to establish connection, retrying");
Logger.LogInformation("Failed to establish connection, retrying");
await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false);
}
}
}
public async Task<ConnectionDto> GetConnectionDto()
{
var dto = await _mareHub!.InvokeAsync<ConnectionDto>(nameof(GetConnectionDto)).ConfigureAwait(false);
Mediator.Publish(new ConnectedMessage(dto));
return dto;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_healthCheckTokenSource?.Cancel();
Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false));
_connectionCancellationTokenSource?.Cancel();
}
private async Task ClientHealthCheck(CancellationToken ct)
{
while (!ct.IsCancellationRequested && _mareHub != null)
{
await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false);
_ = await CheckClientHealth().ConfigureAwait(false);
_logger.LogDebug("Checked Client Health State");
Logger.LogDebug("Checked Client Health State");
}
}
private void DalamudUtilOnLogIn()
{
Task.Run(() => CreateConnections(forceGetToken: true));
}
private void DalamudUtilOnLogOut()
{
Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false));
ServerState = ServerState.Offline;
}
private async Task InitializeData()
{
if (_mareHub == null) return;
_logger.LogDebug("Initializing data");
Logger.LogDebug("Initializing data");
OnDownloadReady((guid) => Client_DownloadReady(guid));
OnReceiveServerMessage((sev, msg) => Client_ReceiveServerMessage(sev, msg));
OnUpdateSystemInfo((dto) => Client_UpdateSystemInfo(dto));
@@ -248,6 +243,7 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH
OnUserSendOnline(dto => Client_UserSendOnline(dto));
OnUserUpdateOtherPairPermissions(dto => Client_UserUpdateOtherPairPermissions(dto));
OnUserUpdateSelfPairPermissions(dto => Client_UserUpdateSelfPairPermissions(dto));
OnUserReceiveUploadStatus(dto => Client_UserReceiveUploadStatus(dto));
OnGroupChangePermissions((dto) => Client_GroupChangePermissions(dto));
OnGroupDelete((dto) => Client_GroupDelete(dto));
@@ -260,12 +256,12 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH
foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false))
{
_logger.LogDebug("Individual Pair: {userPair}", userPair);
Logger.LogDebug("Individual Pair: {userPair}", userPair);
_pairManager.AddUserPair(userPair, addToLastAddedUser: false);
}
foreach (var entry in await GroupsGetAll().ConfigureAwait(false))
{
_logger.LogDebug("Group: {entry}", entry);
Logger.LogDebug("Group: {entry}", entry);
_pairManager.AddGroup(entry);
}
foreach (var group in _pairManager.GroupPairs.Keys)
@@ -273,14 +269,14 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH
var users = await GroupsGetUsersInGroup(group).ConfigureAwait(false);
foreach (var user in users)
{
_logger.LogDebug("Group Pair: {user}", user);
Logger.LogDebug("Group Pair: {user}", user);
_pairManager.AddGroupPair(user);
}
}
foreach (var entry in await UserGetOnlinePairs().ConfigureAwait(false))
{
_pairManager.MarkPairOnline(entry, this, sendNotif: false);
_pairManager.MarkPairOnline(entry, sendNotif: false);
}
_healthCheckTokenSource?.Cancel();
@@ -289,47 +285,25 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH
_ = ClientHealthCheck(_healthCheckTokenSource.Token);
_initialized = true;
Mediator.Publish(new ConnectedMessage());
}
public override void Dispose()
{
base.Dispose();
_healthCheckTokenSource?.Cancel();
_uploadCancellationTokenSource?.Cancel();
Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false));
_connectionCancellationTokenSource?.Cancel();
}
private void MareHubOnClosed(Exception? arg)
{
CurrentUploads.Clear();
CurrentDownloads.Clear();
_uploadCancellationTokenSource?.Cancel();
_healthCheckTokenSource?.Cancel();
Mediator.Publish(new DisconnectedMessage());
_pairManager.ClearPairs();
ServerState = ServerState.Offline;
if (arg != null)
{
_logger.LogWarning(arg, "Connection closed");
Logger.LogWarning(arg, "Connection closed");
}
else
{
_logger.LogInformation("Connection closed");
Logger.LogInformation("Connection closed");
}
}
private void MareHubOnReconnecting(Exception? arg)
{
_doNotNotifyOnNextInfo = true;
_healthCheckTokenSource?.Cancel();
ServerState = ServerState.Reconnecting;
Mediator.Publish(new NotificationMessage("Connection lost", "Connection lost to " + _serverManager.CurrentServer!.ServerName, NotificationType.Warning, 5000));
_logger.LogWarning(arg, "Connection closed... Reconnecting");
}
private async void MareHubOnReconnected(string? arg)
private async Task MareHubOnReconnected()
{
ServerState = ServerState.Connecting;
try
@@ -345,10 +319,17 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Failure to obtain data after reconnection");
Logger.LogCritical(ex, "Failure to obtain data after reconnection");
await StopConnection(ServerState.Disconnected).ConfigureAwait(false);
}
}
private void MareHubOnReconnecting(Exception? arg)
{
_doNotNotifyOnNextInfo = true;
_healthCheckTokenSource?.Cancel();
ServerState = ServerState.Reconnecting;
Logger.LogWarning(arg, "Connection closed... Reconnecting");
}
private async Task StopConnection(ServerState state)
@@ -359,24 +340,11 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH
{
_initialized = false;
_healthCheckTokenSource?.Cancel();
_uploadCancellationTokenSource?.Cancel();
_logger.LogInformation("Stopping existing connection");
Logger.LogInformation("Stopping existing connection");
await _hubFactory.DisposeHubAsync().ConfigureAwait(false);
CurrentUploads.Clear();
CurrentDownloads.Clear();
Mediator.Publish(new DisconnectedMessage());
_mareHub = null;
_connectionDto = null;
}
}
public async Task<ConnectionDto> GetConnectionDto()
{
return await _mareHub!.InvokeAsync<ConnectionDto>(nameof(GetConnectionDto)).ConfigureAwait(false);
}
public async Task<bool> CheckClientHealth()
{
return await _mareHub!.InvokeAsync<bool>(nameof(CheckClientHealth)).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,117 @@
using MareSynchronos.API.SignalR;
using MareSynchronos.Interop;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.WebAPI.SignalR.Utils;
using MessagePack;
using MessagePack.Resolvers;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.WebAPI.SignalR;
public class HubFactory : MediatorSubscriberBase
{
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly MareConfigService _configService;
private HubConnection? _instance;
private bool _isDisposed = false;
public HubFactory(ILogger<HubFactory> logger, MareMediator mediator, ServerConfigurationManager serverConfigurationManager, MareConfigService configService) : base(logger, mediator)
{
_serverConfigurationManager = serverConfigurationManager;
_configService = configService;
}
private HubConnection BuildHubConnection()
{
Logger.LogDebug("Building new HubConnection");
_instance = new HubConnectionBuilder()
.WithUrl(_serverConfigurationManager.CurrentApiUrl + IMareHub.Path, options =>
{
options.Headers.Add("Authorization", "Bearer " + _serverConfigurationManager.GetToken());
options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling;
})
.AddMessagePackProtocol(opt =>
{
var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance,
BuiltinResolver.Instance,
AttributeFormatterResolver.Instance,
// replace enum resolver
DynamicEnumAsStringResolver.Instance,
DynamicGenericResolver.Instance,
DynamicUnionResolver.Instance,
DynamicObjectResolver.Instance,
PrimitiveObjectResolver.Instance,
// final fallback(last priority)
StandardResolver.Instance);
opt.SerializerOptions =
MessagePackSerializerOptions.Standard
.WithCompression(MessagePackCompression.Lz4Block)
.WithResolver(resolver);
})
.WithAutomaticReconnect(new ForeverRetryPolicy(Mediator))
.ConfigureLogging(a =>
{
a.ClearProviders().AddProvider(new DalamudLoggingProvider(_configService));
a.SetMinimumLevel(LogLevel.Information);
})
.Build();
_instance.Closed += HubOnClosed;
_instance.Reconnecting += HubOnReconnecting;
_instance.Reconnected += HubOnReconnected;
_isDisposed = false;
return _instance;
}
private Task HubOnReconnected(string? arg)
{
Mediator.Publish(new HubReconnectedMessage(arg));
return Task.CompletedTask;
}
private Task HubOnReconnecting(Exception? arg)
{
Mediator.Publish(new HubReconnectingMessage(arg));
return Task.CompletedTask;
}
private Task HubOnClosed(Exception? arg)
{
Mediator.Publish(new HubClosedMessage(arg));
return Task.CompletedTask;
}
public HubConnection GetOrCreate()
{
if (!_isDisposed && _instance != null) return _instance;
return BuildHubConnection();
}
public async Task DisposeHubAsync()
{
if (_instance == null || _isDisposed) return;
Logger.LogDebug("Disposing current HubConnection");
_isDisposed = true;
_instance.Closed -= HubOnClosed;
_instance.Reconnecting -= HubOnReconnecting;
_instance.Reconnected -= HubOnReconnected;
await _instance.StopAsync().ConfigureAwait(false);
await _instance.DisposeAsync().ConfigureAwait(false);
_instance = null;
}
}

View File

@@ -1,7 +1,8 @@
using MareSynchronos.Mediator;
using Dalamud.Interface.Internal.Notifications;
using MareSynchronos.Services.Mediator;
using Microsoft.AspNetCore.SignalR.Client;
namespace MareSynchronos.WebAPI.Utils;
namespace MareSynchronos.WebAPI.SignalR.Utils;
public class ForeverRetryPolicy : IRetryPolicy
{
@@ -19,14 +20,17 @@ public class ForeverRetryPolicy : IRetryPolicy
if (retryContext.PreviousRetryCount == 0)
{
_sentDisconnected = false;
timeToWait = TimeSpan.FromSeconds(1);
timeToWait = TimeSpan.FromSeconds(3);
}
else if (retryContext.PreviousRetryCount == 1) timeToWait = TimeSpan.FromSeconds(2);
else if (retryContext.PreviousRetryCount == 2) timeToWait = TimeSpan.FromSeconds(3);
else if (retryContext.PreviousRetryCount == 1) timeToWait = TimeSpan.FromSeconds(5);
else if (retryContext.PreviousRetryCount == 2) timeToWait = TimeSpan.FromSeconds(10);
else
{
if (!_sentDisconnected)
{
_mediator.Publish(new NotificationMessage("Connection lost", "Connection lost to server", NotificationType.Warning, 5000));
_mediator.Publish(new DisconnectedMessage());
}
_sentDisconnected = true;
}

View File

@@ -1,4 +1,4 @@
namespace MareSynchronos.WebAPI;
namespace MareSynchronos.WebAPI.SignalR.Utils;
public enum ServerState
{