Files
SocialPub/PrivaPub/Controllers/ClientToServer/StatisticsController.cs
T
thepraandClaude Opus 5.5 dc63fa57b8 A login's storage is counted, and can have a quota
MediaAttachment.Size was stored and never summed: nobody, the administrator included, could tell what media took,
and nothing bounded it. Counted.Media and MediaOfRoot sum what is kept (not trashed). A login sees what its personas'
uploads and pictures take at /clientapi/user/storage (ViewStorage), with its quota when the server sets one;
Media:QuotaBytesPerRoot (0, no quota, by default) refuses an upload or a picture over it with 422, whichever persona
sends it; the statistics overview gains the media totals, the proxy cache and the trash (ViewMediaTotals). Tests: a
login's storage counts what its personas keep and forgets what is trashed; two uploads from two personas of one login
are refused together past the quota.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-07 11:03:25 +02:00

87 lines
3.8 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PrivaPub.ClientModels;
using PrivaPub.Domain.Statistics;
using PrivaPub.Infrastructure.Jobs;
using PrivaPub.Infrastructure.Statistics;
using PrivaPub.Models.Jobs;
using System.Globalization;
namespace PrivaPub.Controllers.ClientToServer
{
[ApiController,
Route("clientapi/admin/statistics"),
Authorize(Policy = Policies.IsAdmin)]
public class StatisticsController : ControllerBase
{
readonly StatisticsQueries _queries;
readonly IJobQueue _queue;
readonly Microsoft.Extensions.Options.IOptionsMonitor<StatisticsOptions> _options;
public StatisticsController(StatisticsQueries queries, IJobQueue queue, Microsoft.Extensions.Options.IOptionsMonitor<StatisticsOptions> options)
{
_queries = queries;
_queue = queue;
_options = options;
}
[HttpGet, Route("/clientapi/admin/statistics/crawler")]
public async Task<IActionResult> Crawler(CancellationToken token) =>
Ok(await _queries.Crawler(_options.CurrentValue.Crawler.Enabled, _options.CurrentValue.Crawler.RevisitDays, token));
[HttpGet, Route("/clientapi/admin/statistics/overview")]
public async Task<IActionResult> Overview([FromServices] Domain.Media.IMediaService media, [FromQuery] int days = 30, CancellationToken token = default)
{
var overview = await _queries.Overview(days, token);
overview.Media.ProxyCacheBytes = Bytes(media.ProxyRoot);
overview.Media.TrashBytes = Bytes(media.TrashRoot);
return Ok(overview);
}
static long Bytes(string root) =>
Directory.Exists(root) ? new DirectoryInfo(root).EnumerateFiles("*", SearchOption.AllDirectories).Sum(f => f.Length) : 0;
[HttpGet, Route("/clientapi/admin/statistics/hosts")]
public async Task<IActionResult> Hosts([FromQuery] int days = 30, [FromQuery] string sort = default, [FromQuery] string software = default,
[FromQuery] int page = 1, [FromQuery] int limit = 50, CancellationToken token = default) =>
Ok(await _queries.Hosts(days, sort, software, page, limit, token));
[HttpGet, Route("/clientapi/admin/statistics/hosts/{host}")]
public async Task<IActionResult> Host(string host, [FromQuery] int days = 90, CancellationToken token = default) =>
await _queries.Host(host, days, token) is { } found ? Ok(found) : NotFound();
[HttpGet, Route("/clientapi/admin/statistics/events")]
public async Task<IActionResult> Events([FromQuery] string host = default, [FromQuery] string channel = default, [FromQuery] string outcome = default,
[FromQuery] string reason = default, [FromQuery] DateTime? before = default, [FromQuery] int limit = 100, CancellationToken token = default) =>
Ok(await _queries.Events(host, channel, outcome, reason, before, limit, token));
[HttpGet, Route("/clientapi/admin/statistics/server")]
public async Task<IActionResult> Server([FromQuery] int days = 30, CancellationToken token = default) =>
Ok(await _queries.Server(days, token));
[HttpPost, Route("/clientapi/admin/statistics/rollups/{day}")]
public async Task<IActionResult> Rollup(string day, CancellationToken token)
{
if (!DateTime.TryParseExact(day, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var parsed)
|| parsed.Date >= DateTime.UtcNow.Date)
return BadRequest();
var job = RollupJob.For(parsed.Date, $"rollup|{day}|rerun|{DateTime.UtcNow.Ticks}");
job.RunAt = DateTime.UtcNow;
await _queue.EnqueueMany(new[] { job }, token);
return Accepted();
}
[HttpPost, Route("/clientapi/admin/statistics/hosts/{host}/describe")]
public async Task<IActionResult> Describe(string host, CancellationToken token)
{
host = Interactions.Host(host);
if (host == Interactions.Unknown)
return BadRequest();
await _queue.Enqueue(JobKind.DescribeInstance, host, host, $"describe|{host}|admin|{DateTime.UtcNow.Ticks}", token);
return Accepted();
}
}
}