2.0.0 (#92)
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s

2.0.0 Changes:

- Reworked shell finder UI with compact or list view with profile tags showing with the listing, allowing moderators to broadcast the syncshell as well to have it be used more.
- Reworked user list in syncshell admin screen to have filter visible and moved away from table to its own thing, allowing to copy uid/note/alias when clicking on the name.
- Reworked download bars and download box to make it look more modern, removed the jitter around, so it shouldn't vibrate around much.
- Chat has been added to the top menu, working in Zone or in Syncshells to be used there.
- Paired system has been revamped to make pausing and unpausing faster, and loading people should be faster as well.
- Moved to the internal object table to have faster load times for users; people should load in faster
- Compactor is running on a multi-threaded level instead of single-threaded; this should increase the speed of compacting files
- Nameplate Service has been reworked so it wouldn't use the nameplate handler anymore.
- Files can be resized when downloading to reduce load on users if they aren't compressed. (can be toggled to resize all).
- Penumbra Collections are now only made when people are visible, reducing the load on boot-up when having many syncshells in your list.
- Lightfinder plates have been moved away from using Nameplates, but will use an overlay.
- Main UI has been changed a bit with a gradient, and on hover will glow up now.
- Reworked Profile UI for Syncshell and Users to be more user-facing with more customizable items.
- Reworked Settings UI to look more modern.
- Performance should be better due to new systems that would dispose of the collections and better caching of items.

Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: choco <choco@patat.nl>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: Minmoose <KennethBohr@outlook.com>
Reviewed-on: #92
This commit was merged in pull request #92.
This commit is contained in:
2025-12-21 17:19:34 +00:00
parent 906f401940
commit 835a0a637d
191 changed files with 32636 additions and 8841 deletions

View File

@@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Net.Sockets;
using System.Reflection;
namespace LightlessSync.WebAPI.Files;
@@ -84,27 +85,46 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
bool withToken = true)
{
using var requestMessage = new HttpRequestMessage(method, uri);
return await SendRequestInternalAsync(requestMessage, ct, httpCompletionOption, withToken).ConfigureAwait(false);
return await SendRequestInternalAsync(() => new HttpRequestMessage(method, uri),
ct, httpCompletionOption, withToken, allowRetry: true).ConfigureAwait(false);
}
public async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct,
bool withToken = true) where T : class
{
using var requestMessage = new HttpRequestMessage(method, uri);
if (content is not ByteArrayContent)
requestMessage.Content = JsonContent.Create(content);
else
requestMessage.Content = content as ByteArrayContent;
return await SendRequestInternalAsync(requestMessage, ct, withToken: withToken).ConfigureAwait(false);
return await SendRequestInternalAsync(() =>
{
var requestMessage = new HttpRequestMessage(method, uri);
if (content is not ByteArrayContent byteArrayContent)
{
requestMessage.Content = JsonContent.Create(content);
}
else
{
var clonedContent = new ByteArrayContent(byteArrayContent.ReadAsByteArrayAsync().GetAwaiter().GetResult());
foreach (var header in byteArrayContent.Headers)
{
clonedContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
requestMessage.Content = clonedContent;
}
return requestMessage;
}, ct, HttpCompletionOption.ResponseContentRead, withToken,
allowRetry: content is not HttpContent || content is ByteArrayContent).ConfigureAwait(false);
}
public async Task<HttpResponseMessage> SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content,
CancellationToken ct, bool withToken = true)
{
using var requestMessage = new HttpRequestMessage(method, uri);
requestMessage.Content = content;
return await SendRequestInternalAsync(requestMessage, ct, withToken: withToken).ConfigureAwait(false);
return await SendRequestInternalAsync(() =>
{
var requestMessage = new HttpRequestMessage(method, uri)
{
Content = content
};
return requestMessage;
}, ct, HttpCompletionOption.ResponseContentRead, withToken, allowRetry: false).ConfigureAwait(false);
}
public async Task WaitForDownloadSlotAsync(CancellationToken token)
@@ -146,39 +166,78 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
return Math.Clamp(dividedLimit, 1, long.MaxValue);
}
private async Task<HttpResponseMessage> SendRequestInternalAsync(HttpRequestMessage requestMessage,
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, bool withToken = true)
private async Task<HttpResponseMessage> SendRequestInternalAsync(Func<HttpRequestMessage> requestFactory,
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
bool withToken = true, bool allowRetry = true)
{
if (withToken)
{
var token = await _tokenProvider.GetToken().ConfigureAwait(false);
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
const int maxAttempts = 2;
var attempt = 0;
if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent)
while (true)
{
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);
}
attempt++;
using var requestMessage = requestFactory();
try
{
if (ct != null)
return await _httpClient.SendAsync(requestMessage, httpCompletionOption, ct.Value).ConfigureAwait(false);
return await _httpClient.SendAsync(requestMessage, httpCompletionOption).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
throw;
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during SendRequestInternal for {uri}", requestMessage.RequestUri);
throw;
if (withToken)
{
var token = await _tokenProvider.GetToken().ConfigureAwait(false);
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent)
{
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, httpCompletionOption, ct.Value).ConfigureAwait(false);
return await _httpClient.SendAsync(requestMessage, httpCompletionOption).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
throw;
}
catch (Exception ex) when (allowRetry && attempt < maxAttempts && IsTransientNetworkException(ex))
{
Logger.LogWarning(ex, "Transient error during SendRequestInternal for {uri}, retrying attempt {attempt}/{maxAttempts}",
requestMessage.RequestUri, attempt, maxAttempts);
if (ct.HasValue)
{
await Task.Delay(TimeSpan.FromMilliseconds(200), ct.Value).ConfigureAwait(false);
}
else
{
await Task.Delay(TimeSpan.FromMilliseconds(200)).ConfigureAwait(false);
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during SendRequestInternal for {uri}", requestMessage.RequestUri);
throw;
}
}
}
private static bool IsTransientNetworkException(Exception ex)
{
var current = ex;
while (current != null)
{
if (current is SocketException socketEx)
{
return socketEx.SocketErrorCode is SocketError.ConnectionReset or SocketError.ConnectionAborted or SocketError.TimedOut;
}
current = current.InnerException;
}
return false;
}
}