using Synology.Api.Client;
namespace RdtClient.Service.Services.Downloaders;
///
/// Holds a single authenticated shared across all Download Station downloads.
/// Logging in once instead of once per download avoids the concurrent-login session churn that made DSM return
/// error 119 "SID not found" under high file-count concurrency. The session is created on demand, reused while
/// the credentials are unchanged, and dropped (via ) when a download hits a session
/// error so the next caller logs in again.
///
public static class SynologyClientProvider
{
private static readonly Func> DefaultClientFactory = async (url, username, password) =>
{
var client = new SynologyClient(url);
await client.LoginAsync(username, password);
return client;
};
private static readonly SemaphoreSlim Gate = new(1, 1);
private static ISynologyClient? _client;
private static (String Url, String Username, String Password)? _key;
///
/// Builds an authenticated client. Overridable so tests can supply a fake without a live DSM. Restored by
/// .
///
public static Func> ClientFactory { get; set; } = DefaultClientFactory;
///
/// Returns the shared authenticated client for these credentials, logging in once. Concurrent first calls
/// collapse to a single login; later calls reuse the cached client.
///
public static async Task GetClientAsync(String url, String username, String password)
{
var key = (url, username, password);
// Fast path: a client for these exact credentials already exists.
var current = _client;
if (current != null && _key == key)
{
return current;
}
await Gate.WaitAsync();
try
{
if (_client != null && _key == key)
{
return _client;
}
// No client yet, or the credentials changed: drop any stale session and authenticate fresh.
await TryLogoutAsync(_client);
_client = await ClientFactory(url, username, password);
_key = key;
return _client;
}
finally
{
Gate.Release();
}
}
///
/// Drops the cached session so the next authenticates again. Pass the client that
/// just failed so a session another caller has already refreshed is not thrown away.
///
public static async Task InvalidateAsync(ISynologyClient? failedClient = null)
{
await Gate.WaitAsync();
try
{
if (failedClient != null && !ReferenceEquals(_client, failedClient))
{
return;
}
await TryLogoutAsync(_client);
_client = null;
_key = null;
}
finally
{
Gate.Release();
}
}
///
/// Clears the cached session and restores the default factory. For test isolation.
///
public static void Reset()
{
_client = null;
_key = null;
ClientFactory = DefaultClientFactory;
}
private static async Task TryLogoutAsync(ISynologyClient? client)
{
if (client == null)
{
return;
}
try
{
await client.LogoutAsync();
}
catch
{
// Best-effort: an expired or dead session cannot be logged out, and that is fine.
}
}
}