[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:
@@ -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 _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
341
MareSynchronos/WebAPI/Files/FileDownloadManager.cs
Normal file
341
MareSynchronos/WebAPI/Files/FileDownloadManager.cs
Normal 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 _);
|
||||
}
|
||||
}
|
||||
}
|
||||
109
MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs
Normal file
109
MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
226
MareSynchronos/WebAPI/Files/FileUploadManager.cs
Normal file
226
MareSynchronos/WebAPI/Files/FileUploadManager.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
10
MareSynchronos/WebAPI/Files/Models/DownloadStatus.cs
Normal file
10
MareSynchronos/WebAPI/Files/Models/DownloadStatus.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace MareSynchronos.WebAPI.Files.Models;
|
||||
|
||||
public enum DownloadStatus
|
||||
{
|
||||
Initializing,
|
||||
WaitingForSlot,
|
||||
WaitingForQueue,
|
||||
Downloading,
|
||||
Decompressing
|
||||
}
|
||||
10
MareSynchronos/WebAPI/Files/Models/FileDownloadStatus.cs
Normal file
10
MareSynchronos/WebAPI/Files/Models/FileDownloadStatus.cs
Normal 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; }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using MareSynchronos.API.Dto.Files;
|
||||
|
||||
namespace MareSynchronos.WebAPI.Utils;
|
||||
namespace MareSynchronos.WebAPI.Files.Models;
|
||||
|
||||
public abstract class FileTransfer
|
||||
{
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using MareSynchronos.API.Dto.Files;
|
||||
|
||||
namespace MareSynchronos.WebAPI.Utils;
|
||||
namespace MareSynchronos.WebAPI.Files.Models;
|
||||
|
||||
public class UploadFileTransfer : FileTransfer
|
||||
{
|
||||
3
MareSynchronos/WebAPI/Files/Models/UploadProgress.cs
Normal file
3
MareSynchronos/WebAPI/Files/Models/UploadProgress.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace MareSynchronos.WebAPI.Files.Models;
|
||||
|
||||
public record UploadProgress(long Uploaded, long Size);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
117
MareSynchronos/WebAPI/SignalR/HubFactory.cs
Normal file
117
MareSynchronos/WebAPI/SignalR/HubFactory.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace MareSynchronos.WebAPI;
|
||||
namespace MareSynchronos.WebAPI.SignalR.Utils;
|
||||
|
||||
public enum ServerState
|
||||
{
|
||||
Reference in New Issue
Block a user