146 lines
5.4 KiB
C#
146 lines
5.4 KiB
C#
using Microsoft.Extensions.Options;
|
|||
|
|
using Microsoft.Extensions.Primitives;
|
||
|
|
|
||
|
|
using MongoDB.Bson.Serialization;
|
||
|
|
using MongoDB.Entities;
|
||
|
|
|
||
|
|
using PrivaPub.Domain.Media;
|
||
|
|
using PrivaPub.Models;
|
||
|
|
|
||
|
|
namespace PrivaPub.Domain.Instance
|
||
|
|
{
|
||
|
|
public interface IInstanceSettings
|
||
|
|
{
|
||
|
|
/// <summary>The settings as they are now, never null: the defaults (today's literals) until an administrator saves.
|
||
|
|
/// Never change it: Update does.</summary>
|
||
|
|
ServerSettings Current { get; }
|
||
|
|
/// <summary>Reads the document again; tells whoever listens (ChangeToken) when it changed.</summary>
|
||
|
|
Task Reload(CancellationToken token);
|
||
|
|
/// <summary>Changes the settings as stored now (or the defaults when none are), saves them and makes them current.</summary>
|
||
|
|
Task<ServerSettings> Update(Action<ServerSettings> change, string byRootId, CancellationToken token);
|
||
|
|
/// <summary>Fires once the settings change (a save here, or a reload that found another version).</summary>
|
||
|
|
IChangeToken ChangeToken();
|
||
|
|
}
|
||
|
|
|
||
|
|
// The server's own settings (Models/ServerSettings.cs) in memory: read at start, saved from the administrator's page, and
|
||
|
|
// read again every five minutes (InstanceSettingsRefresher), so an edit made elsewhere takes too. What reads them reads
|
||
|
|
// Current, which is never null and never changed in place.
|
||
|
|
public class InstanceSettings : IInstanceSettings
|
||
|
|
{
|
||
|
|
readonly ILogger<InstanceSettings> _logger;
|
||
|
|
readonly SemaphoreSlim _saving = new(1, 1);
|
||
|
|
volatile ServerSettings _current = ServerSettings.Defaults();
|
||
|
|
volatile bool _stored;//whether Current came from the document (or is the defaults)
|
||
|
|
CancellationTokenSource _changed = new();
|
||
|
|
|
||
|
|
public InstanceSettings(ILogger<InstanceSettings> logger)
|
||
|
|
{
|
||
|
|
_logger = logger;
|
||
|
|
}
|
||
|
|
|
||
|
|
public ServerSettings Current => _current;
|
||
|
|
|
||
|
|
public IChangeToken ChangeToken() => new CancellationChangeToken(Volatile.Read(ref _changed).Token);
|
||
|
|
|
||
|
|
public async Task Reload(CancellationToken token)
|
||
|
|
{
|
||
|
|
var stored = await Find(token);
|
||
|
|
if ((stored != default) == _stored && stored?.UpdatedAt == _current.UpdatedAt)
|
||
|
|
return;
|
||
|
|
Swap(stored ?? ServerSettings.Defaults(), stored != default);
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<ServerSettings> Update(Action<ServerSettings> change, string byRootId, CancellationToken token)
|
||
|
|
{
|
||
|
|
await _saving.WaitAsync(token);
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var settings = await Find(token) ?? ServerSettings.Defaults();
|
||
|
|
change(settings);
|
||
|
|
settings.ID = ServerSettings.Name;
|
||
|
|
settings.UpdatedAt = DateTime.UtcNow;
|
||
|
|
settings.UpdatedBy = byRootId;
|
||
|
|
await DB.Default.SaveAsync(settings, token);
|
||
|
|
// what is kept in memory is a copy nobody else holds
|
||
|
|
Swap(BsonSerializer.Deserialize<ServerSettings>(MongoDB.Bson.BsonExtensionMethods.ToBson(settings)), stored: true);
|
||
|
|
return settings;
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
_saving.Release();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static Task<ServerSettings> Find(CancellationToken token) =>
|
||
|
|
DB.Default.Find<ServerSettings>().Match(s => s.ID == ServerSettings.Name).ExecuteFirstAsync(token);
|
||
|
|
|
||
|
|
void Swap(ServerSettings settings, bool stored)
|
||
|
|
{
|
||
|
|
_current = settings;
|
||
|
|
_stored = stored;
|
||
|
|
var previous = Interlocked.Exchange(ref _changed, new CancellationTokenSource());
|
||
|
|
try
|
||
|
|
{
|
||
|
|
previous.Cancel();
|
||
|
|
}
|
||
|
|
catch (AggregateException ex)
|
||
|
|
{
|
||
|
|
_logger.LogWarning(ex, "A listener to the server's settings failed");
|
||
|
|
}
|
||
|
|
//(not disposed: a reader may still be registering on its token)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// reads the settings again every five minutes, so an edit made elsewhere (another process, the database) takes
|
||
|
|
public sealed class InstanceSettingsRefresher(IInstanceSettings settings, ILogger<InstanceSettingsRefresher> logger) : BackgroundService
|
||
|
|
{
|
||
|
|
static readonly TimeSpan Every = TimeSpan.FromMinutes(5);
|
||
|
|
|
||
|
|
protected override async Task ExecuteAsync(CancellationToken token)
|
||
|
|
{
|
||
|
|
using var timer = new PeriodicTimer(Every);
|
||
|
|
while (await timer.WaitForNextTickAsync(token))
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
await settings.Reload(token);
|
||
|
|
}
|
||
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||
|
|
{
|
||
|
|
logger.LogWarning(ex, "The server's settings could not be read again");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Media:* as the administrator changed it (ServerSettings.Media), within hard caps: images up to 40 MB, video up to
|
||
|
|
// 99 MB (nginx takes 100 MB, as does MediaController's request limit), a quota of 0 or more, audio and video up to four
|
||
|
|
// hours. Every IOptionsMonitor<MediaOptions> reader sees it, since the settings' change token is one of its sources.
|
||
|
|
public sealed class MediaLimitsFromSettings(IInstanceSettings settings) : IPostConfigureOptions<MediaOptions>
|
||
|
|
{
|
||
|
|
public const long MaxImageBytes = 40L * 1024 * 1024;
|
||
|
|
public const long MaxVideoBytes = 99L * 1024 * 1024;
|
||
|
|
public const int MaxSeconds = 4 * 3600;
|
||
|
|
|
||
|
|
public void PostConfigure(string name, MediaOptions options)
|
||
|
|
{
|
||
|
|
if (name != Options.DefaultName || settings.Current.Media is not { } media)
|
||
|
|
return;
|
||
|
|
if (media.QuotaBytesPerRoot is { } quota)
|
||
|
|
options.QuotaBytesPerRoot = Math.Max(0, quota);
|
||
|
|
if (media.MaxImageBytes is { } image)
|
||
|
|
options.MaxImageBytes = Math.Clamp(image, 1, MaxImageBytes);
|
||
|
|
if (media.MaxVideoBytes is { } video)
|
||
|
|
options.MaxVideoBytes = Math.Clamp(video, 1, MaxVideoBytes);
|
||
|
|
if (media.MaxSeconds is { } seconds)
|
||
|
|
options.MaxSeconds = Math.Clamp(seconds, 1, MaxSeconds);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public sealed class MediaLimitsChangeSource(IInstanceSettings settings) : IOptionsChangeTokenSource<MediaOptions>
|
||
|
|
{
|
||
|
|
public string Name => Options.DefaultName;
|
||
|
|
|
||
|
|
public IChangeToken GetChangeToken() => settings.ChangeToken();
|
||
|
|
}
|
||
|
|
}
|