mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-04-10 22:39:05 +02:00
Decouple user upload store
This commit is contained in:
parent
1f484f243d
commit
cba4d5fc80
@ -68,8 +68,7 @@ namespace VoidCat.Controllers
|
||||
Name = filename,
|
||||
Description = Request.Headers.GetHeader("V-Description"),
|
||||
Digest = Request.Headers.GetHeader("V-Full-Digest"),
|
||||
Size = (ulong?) Request.ContentLength ?? 0UL,
|
||||
Uploader = uid
|
||||
Size = (ulong?) Request.ContentLength ?? 0UL
|
||||
};
|
||||
|
||||
var digest = Request.Headers.GetHeader("V-Digest");
|
||||
@ -80,13 +79,13 @@ namespace VoidCat.Controllers
|
||||
|
||||
// save metadata
|
||||
await _metadata.Set(vf.Id, vf.Metadata!);
|
||||
|
||||
|
||||
// attach file upload to user
|
||||
if (uid.HasValue)
|
||||
{
|
||||
await _userUploads.AddFile(uid!.Value, vf);
|
||||
}
|
||||
|
||||
|
||||
if (cli)
|
||||
{
|
||||
var urlBuilder = new UriBuilder(Request.IsHttps ? "https" : "http", Request.Host.Host,
|
||||
@ -137,7 +136,7 @@ namespace VoidCat.Controllers
|
||||
Id = gid,
|
||||
IsAppend = true
|
||||
}, HttpContext.RequestAborted);
|
||||
|
||||
|
||||
// update file size
|
||||
await _metadata.Set(vf.Id, vf.Metadata!);
|
||||
return UploadResult.Success(vf);
|
||||
@ -155,9 +154,13 @@ namespace VoidCat.Controllers
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
[Route("{id}")]
|
||||
public ValueTask<PublicVoidFile?> GetInfo([FromRoute] string id)
|
||||
public async Task<IActionResult> GetInfo([FromRoute] string id)
|
||||
{
|
||||
return _fileInfo.Get(id.FromBase58Guid());
|
||||
var fid = id.FromBase58Guid();
|
||||
var uid = HttpContext.GetUserId();
|
||||
var isOwner = uid.HasValue && await _userUploads.Uploader(fid) == uid;
|
||||
|
||||
return isOwner ? Json(await _fileInfo.GetPrivate(fid)) : Json(await _fileInfo.Get(fid));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -207,7 +210,7 @@ namespace VoidCat.Controllers
|
||||
var gid = id.FromBase58Guid();
|
||||
var meta = await _metadata.Get<SecretVoidFileMeta>(gid);
|
||||
if (meta == default) return NotFound();
|
||||
if (!meta.CanEdit(req.EditSecret, HttpContext)) return Unauthorized();
|
||||
if (!meta.CanEdit(req.EditSecret)) return Unauthorized();
|
||||
|
||||
if (req.Strike != default)
|
||||
{
|
||||
@ -236,7 +239,7 @@ namespace VoidCat.Controllers
|
||||
var gid = id.FromBase58Guid();
|
||||
var meta = await _metadata.Get<SecretVoidFileMeta>(gid);
|
||||
if (meta == default) return NotFound();
|
||||
if (!meta.CanEdit(fileMeta.EditSecret, HttpContext)) return Unauthorized();
|
||||
if (!meta.CanEdit(fileMeta.EditSecret)) return Unauthorized();
|
||||
|
||||
await _metadata.Update(gid, fileMeta);
|
||||
return Ok();
|
||||
|
@ -10,12 +10,14 @@ public class UserController : Controller
|
||||
private readonly IUserStore _store;
|
||||
private readonly IUserUploadsStore _userUploads;
|
||||
private readonly IEmailVerification _emailVerification;
|
||||
private readonly IFileInfoManager _fileInfoManager;
|
||||
|
||||
public UserController(IUserStore store, IUserUploadsStore userUploads, IEmailVerification emailVerification)
|
||||
public UserController(IUserStore store, IUserUploadsStore userUploads, IEmailVerification emailVerification, IFileInfoManager fileInfoManager)
|
||||
{
|
||||
_store = store;
|
||||
_userUploads = userUploads;
|
||||
_emailVerification = emailVerification;
|
||||
_fileInfoManager = fileInfoManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -95,7 +97,15 @@ public class UserController : Controller
|
||||
!user.Flags.HasFlag(VoidUserFlags.PublicUploads)) return Forbid();
|
||||
|
||||
var results = await _userUploads.ListFiles(id.FromBase58Guid(), request);
|
||||
return Json(await results.GetResults());
|
||||
var files = await results.Results.ToListAsync();
|
||||
var fileInfo = await Task.WhenAll(files.Select(a => _fileInfoManager.Get(a).AsTask()));
|
||||
return Json(new RenderedResults<PublicVoidFile>()
|
||||
{
|
||||
PageSize = results.PageSize,
|
||||
Page = results.Page,
|
||||
TotalResults = results.TotalResults,
|
||||
Results = fileInfo.Where(a => a != null).ToList()!
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -61,10 +61,9 @@ public static class Extensions
|
||||
return !string.IsNullOrEmpty(h.Value.ToString()) ? h.Value.ToString() : default;
|
||||
}
|
||||
|
||||
public static bool CanEdit(this SecretVoidFileMeta file, Guid? editSecret, HttpContext context)
|
||||
public static bool CanEdit(this SecretVoidFileMeta file, Guid? editSecret)
|
||||
{
|
||||
return file.EditSecret == editSecret
|
||||
|| file.Uploader == context.GetUserId();
|
||||
return file.EditSecret == editSecret;
|
||||
}
|
||||
|
||||
public static string ToHex(this byte[] data)
|
||||
|
@ -65,12 +65,6 @@ public record VoidFileMeta : IVoidFileMeta
|
||||
/// Url to download the file
|
||||
/// </summary>
|
||||
public Uri? Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User who uploaded the file
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(Base58GuidConverter))]
|
||||
public Guid? Uploader { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -158,9 +158,8 @@ services.AddCaptcha(voidSettings);
|
||||
// postgres
|
||||
if (!string.IsNullOrEmpty(voidSettings.Postgres))
|
||||
{
|
||||
services.AddScoped<OpenDatabase>();
|
||||
services.AddScoped((_) => new NpgsqlConnection(voidSettings.Postgres));
|
||||
services.AddScoped<IDbConnection>((svc) => svc.GetRequiredService<NpgsqlConnection>());
|
||||
services.AddSingleton<PostgresConnectionFactory>();
|
||||
services.AddTransient<IDbConnection>(_ => new NpgsqlConnection(voidSettings.Postgres));
|
||||
|
||||
// fluent migrations
|
||||
services.AddTransient<IMigration, FluentMigrationRunner>();
|
||||
@ -221,11 +220,6 @@ app.UseSwaggerUI();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
if (!string.IsNullOrEmpty(voidSettings.Postgres))
|
||||
{
|
||||
app.UseMiddleware<OpenDatabase>();
|
||||
}
|
||||
|
||||
app.UseEndpoints(ep =>
|
||||
{
|
||||
ep.MapControllers();
|
||||
|
@ -14,6 +14,13 @@ public interface IFileInfoManager
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask<PublicVoidFile?> Get(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Get all private metadata for a single file
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask<PrivateVoidFile?> GetPrivate(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Get all metadata for multiple files
|
||||
|
@ -2,8 +2,31 @@ using VoidCat.Model;
|
||||
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Mapping store to associate files to users
|
||||
/// </summary>
|
||||
public interface IUserUploadsStore
|
||||
{
|
||||
ValueTask<PagedResult<PublicVoidFile>> ListFiles(Guid user, PagedRequest request);
|
||||
/// <summary>
|
||||
/// List all files for the user
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask<PagedResult<Guid>> ListFiles(Guid user, PagedRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Assign a file upload to a user
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="voidFile"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask AddFile(Guid user, PrivateVoidFile voidFile);
|
||||
|
||||
/// <summary>
|
||||
/// Get the uploader of a single file
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask<Guid?> Uploader(Guid file);
|
||||
}
|
||||
|
@ -40,8 +40,8 @@ public class DeleteUnverifiedAccounts : BackgroundService
|
||||
// ReSharper disable once UseCancellationTokenForIAsyncEnumerable
|
||||
await foreach (var file in files.Results)
|
||||
{
|
||||
await fileStore.DeleteFile(file.Id);
|
||||
await fileInfoManager.Delete(file.Id);
|
||||
await fileStore.DeleteFile(file);
|
||||
await fileInfoManager.Delete(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,40 +11,29 @@ public class FileInfoManager : IFileInfoManager
|
||||
private readonly IStatsReporter _statsReporter;
|
||||
private readonly IUserStore _userStore;
|
||||
private readonly IVirusScanStore _virusScanStore;
|
||||
private readonly IUserUploadsStore _userUploadsStore;
|
||||
|
||||
public FileInfoManager(IFileMetadataStore metadataStore, IPaywallStore paywallStore, IStatsReporter statsReporter,
|
||||
IUserStore userStore, IVirusScanStore virusScanStore)
|
||||
IUserStore userStore, IVirusScanStore virusScanStore, IUserUploadsStore userUploadsStore)
|
||||
{
|
||||
_metadataStore = metadataStore;
|
||||
_paywallStore = paywallStore;
|
||||
_statsReporter = statsReporter;
|
||||
_userStore = userStore;
|
||||
_virusScanStore = virusScanStore;
|
||||
_userUploadsStore = userUploadsStore;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<PublicVoidFile?> Get(Guid id)
|
||||
public ValueTask<PublicVoidFile?> Get(Guid id)
|
||||
{
|
||||
var meta = _metadataStore.Get<VoidFileMeta>(id);
|
||||
var paywall = _paywallStore.Get(id);
|
||||
var bandwidth = _statsReporter.GetBandwidth(id);
|
||||
var virusScan = _virusScanStore.Get(id);
|
||||
await Task.WhenAll(meta.AsTask(), paywall.AsTask(), bandwidth.AsTask(), virusScan.AsTask());
|
||||
return Get<PublicVoidFile, VoidFileMeta>(id);
|
||||
}
|
||||
|
||||
if (meta.Result == default) return default;
|
||||
|
||||
var uploader = meta.Result?.Uploader;
|
||||
var user = uploader.HasValue ? await _userStore.Get<PublicVoidUser>(uploader.Value) : null;
|
||||
|
||||
return new()
|
||||
{
|
||||
Id = id,
|
||||
Metadata = meta.Result,
|
||||
Paywall = paywall.Result,
|
||||
Bandwidth = bandwidth.Result,
|
||||
Uploader = user?.Flags.HasFlag(VoidUserFlags.PublicProfile) == true ? user : null,
|
||||
VirusScan = virusScan.Result
|
||||
};
|
||||
/// <inheritdoc />
|
||||
public ValueTask<PrivateVoidFile?> GetPrivate(Guid id)
|
||||
{
|
||||
return Get<PrivateVoidFile, SecretVoidFileMeta>(id);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -71,4 +60,28 @@ public class FileInfoManager : IFileInfoManager
|
||||
await _statsReporter.Delete(id);
|
||||
await _virusScanStore.Delete(id);
|
||||
}
|
||||
|
||||
private async ValueTask<TFile?> Get<TFile, TMeta>(Guid id)
|
||||
where TMeta : VoidFileMeta where TFile : VoidFile<TMeta>, new()
|
||||
{
|
||||
var meta = _metadataStore.Get<TMeta>(id);
|
||||
var paywall = _paywallStore.Get(id);
|
||||
var bandwidth = _statsReporter.GetBandwidth(id);
|
||||
var virusScan = _virusScanStore.Get(id);
|
||||
var uploader = _userUploadsStore.Uploader(id);
|
||||
await Task.WhenAll(meta.AsTask(), paywall.AsTask(), bandwidth.AsTask(), virusScan.AsTask(), uploader.AsTask());
|
||||
|
||||
if (meta.Result == default) return default;
|
||||
var user = uploader.Result.HasValue ? await _userStore.Get<PublicVoidUser>(uploader.Result.Value) : null;
|
||||
|
||||
return new TFile()
|
||||
{
|
||||
Id = id,
|
||||
Metadata = meta.Result,
|
||||
Paywall = paywall.Result,
|
||||
Bandwidth = bandwidth.Result,
|
||||
Uploader = user?.Flags.HasFlag(VoidUserFlags.PublicProfile) == true ? user : null,
|
||||
VirusScan = virusScan.Result
|
||||
};
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
@ -8,9 +7,9 @@ namespace VoidCat.Services.Files;
|
||||
/// <inheritdoc />
|
||||
public class PostgresFileMetadataStore : IFileMetadataStore
|
||||
{
|
||||
private readonly NpgsqlConnection _connection;
|
||||
private readonly PostgresConnectionFactory _connection;
|
||||
|
||||
public PostgresFileMetadataStore(NpgsqlConnection connection)
|
||||
public PostgresFileMetadataStore(PostgresConnectionFactory connection)
|
||||
{
|
||||
_connection = connection;
|
||||
}
|
||||
@ -30,7 +29,8 @@ public class PostgresFileMetadataStore : IFileMetadataStore
|
||||
/// <inheritdoc />
|
||||
public async ValueTask Set(Guid id, SecretVoidFileMeta obj)
|
||||
{
|
||||
await _connection.ExecuteAsync(
|
||||
await using var conn = await _connection.Get();
|
||||
await conn.ExecuteAsync(
|
||||
@"insert into
|
||||
""Files""(""Id"", ""Name"", ""Size"", ""Uploaded"", ""Description"", ""MimeType"", ""Digest"", ""EditSecret"")
|
||||
values(:id, :name, :size, :uploaded, :description, :mimeType, :digest, :editSecret)
|
||||
@ -50,20 +50,23 @@ on conflict (""Id"") do update set ""Name"" = :name, ""Description"" = :descript
|
||||
/// <inheritdoc />
|
||||
public async ValueTask Delete(Guid id)
|
||||
{
|
||||
await _connection.ExecuteAsync("delete from \"Files\" where \"Id\" = :id", new {id});
|
||||
await using var conn = await _connection.Get();
|
||||
await conn.ExecuteAsync("delete from \"Files\" where \"Id\" = :id", new {id});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : VoidFileMeta
|
||||
{
|
||||
return await _connection.QuerySingleOrDefaultAsync<TMeta?>(@"select * from ""Files"" where ""Id"" = :id",
|
||||
await using var conn = await _connection.Get();
|
||||
return await conn.QuerySingleOrDefaultAsync<TMeta?>(@"select * from ""Files"" where ""Id"" = :id",
|
||||
new {id});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IReadOnlyList<TMeta>> Get<TMeta>(Guid[] ids) where TMeta : VoidFileMeta
|
||||
{
|
||||
var ret = await _connection.QueryAsync<TMeta>("select * from \"Files\" where \"Id\" in :ids", new {ids});
|
||||
await using var conn = await _connection.Get();
|
||||
var ret = await conn.QueryAsync<TMeta>("select * from \"Files\" where \"Id\" in :ids", new {ids});
|
||||
return ret.ToList();
|
||||
}
|
||||
|
||||
@ -83,7 +86,8 @@ on conflict (""Id"") do update set ""Name"" = :name, ""Description"" = :descript
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<PagedResult<TMeta>> ListFiles<TMeta>(PagedRequest request) where TMeta : VoidFileMeta
|
||||
{
|
||||
var count = await _connection.ExecuteScalarAsync<int>(@"select count(*) from ""Files""");
|
||||
await using var conn = await _connection.Get();
|
||||
var count = await conn.ExecuteScalarAsync<int>(@"select count(*) from ""Files""");
|
||||
|
||||
async IAsyncEnumerable<TMeta> Enumerate()
|
||||
{
|
||||
@ -94,8 +98,9 @@ on conflict (""Id"") do update set ""Name"" = :name, ""Description"" = :descript
|
||||
PagedSortBy.Size => "Size",
|
||||
_ => "Id"
|
||||
};
|
||||
await using var iconn = await _connection.Get();
|
||||
var orderDirection = request.SortOrder == PageSortOrder.Asc ? "asc" : "desc";
|
||||
var results = await _connection.QueryAsync<TMeta>(
|
||||
var results = await iconn.QueryAsync<TMeta>(
|
||||
$"select * from \"Files\" order by \"{orderBy}\" {orderDirection} offset @offset limit @limit",
|
||||
new {offset = request.PageSize * request.Page, limit = request.PageSize});
|
||||
|
||||
@ -117,7 +122,8 @@ on conflict (""Id"") do update set ""Name"" = :name, ""Description"" = :descript
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IFileMetadataStore.StoreStats> Stats()
|
||||
{
|
||||
var v = await _connection.QuerySingleAsync<(long Files, long Size)>(
|
||||
await using var conn = await _connection.Get();
|
||||
var v = await conn.QuerySingleAsync<(long Files, long Size)>(
|
||||
@"select count(1) ""Files"", cast(sum(""Size"") as bigint) ""Size"" from ""Files""");
|
||||
return new(v.Files, (ulong) v.Size);
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
using Npgsql;
|
||||
|
||||
public class OpenDatabase : IMiddleware
|
||||
{
|
||||
private readonly NpgsqlConnection _connection;
|
||||
|
||||
public OpenDatabase(NpgsqlConnection connection)
|
||||
{
|
||||
_connection = connection;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||
{
|
||||
await _connection.OpenAsync();
|
||||
try
|
||||
{
|
||||
await next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _connection.CloseAsync();
|
||||
}
|
||||
}
|
||||
}
|
26
VoidCat/Services/PostgresConnectionFactory.cs
Normal file
26
VoidCat/Services/PostgresConnectionFactory.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.Data;
|
||||
using Npgsql;
|
||||
using VoidCat.Model;
|
||||
|
||||
namespace VoidCat.Services;
|
||||
|
||||
public sealed class PostgresConnectionFactory
|
||||
{
|
||||
private readonly VoidSettings _settings;
|
||||
|
||||
public PostgresConnectionFactory(VoidSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public async Task<NpgsqlConnection> Get()
|
||||
{
|
||||
var conn = new NpgsqlConnection(_settings.Postgres);
|
||||
if (!conn.State.HasFlag(ConnectionState.Open))
|
||||
{
|
||||
await conn.OpenAsync();
|
||||
}
|
||||
|
||||
return conn;
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
using VoidCat.Model;
|
||||
|
||||
namespace VoidCat.Services.Users;
|
||||
@ -7,10 +6,10 @@ namespace VoidCat.Services.Users;
|
||||
/// <inheritdoc />
|
||||
public class PostgresEmailVerification : BaseEmailVerification
|
||||
{
|
||||
private readonly NpgsqlConnection _connection;
|
||||
private readonly PostgresConnectionFactory _connection;
|
||||
|
||||
public PostgresEmailVerification(ILogger<BaseEmailVerification> logger, VoidSettings settings,
|
||||
RazorPartialToStringRenderer renderer, NpgsqlConnection connection) : base(logger, settings, renderer)
|
||||
RazorPartialToStringRenderer renderer, PostgresConnectionFactory connection) : base(logger, settings, renderer)
|
||||
{
|
||||
_connection = connection;
|
||||
}
|
||||
@ -18,7 +17,8 @@ public class PostgresEmailVerification : BaseEmailVerification
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask SaveToken(EmailVerificationCode code)
|
||||
{
|
||||
await _connection.ExecuteAsync(
|
||||
await using var conn = await _connection.Get();
|
||||
await conn.ExecuteAsync(
|
||||
@"insert into ""EmailVerification""(""User"", ""Code"", ""Expires"") values(:user, :code, :expires)",
|
||||
new
|
||||
{
|
||||
@ -31,7 +31,8 @@ public class PostgresEmailVerification : BaseEmailVerification
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<EmailVerificationCode?> GetToken(Guid user, Guid code)
|
||||
{
|
||||
return await _connection.QuerySingleOrDefaultAsync<EmailVerificationCode>(
|
||||
await using var conn = await _connection.Get();
|
||||
return await conn.QuerySingleOrDefaultAsync<EmailVerificationCode>(
|
||||
@"select * from ""EmailVerification"" where ""User"" = :user and ""Code"" = :code",
|
||||
new {user, code});
|
||||
}
|
||||
@ -39,7 +40,9 @@ public class PostgresEmailVerification : BaseEmailVerification
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask DeleteToken(Guid user, Guid code)
|
||||
{
|
||||
await _connection.ExecuteAsync(@"delete from ""EmailVerification"" where ""User"" = :user and ""Code"" = :code",
|
||||
await using var conn = await _connection.Get();
|
||||
await conn.ExecuteAsync(
|
||||
@"delete from ""EmailVerification"" where ""User"" = :user and ""Code"" = :code",
|
||||
new {user, code});
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
@ -8,9 +7,9 @@ namespace VoidCat.Services.Users;
|
||||
/// <inheritdoc />
|
||||
public class PostgresUserStore : IUserStore
|
||||
{
|
||||
private readonly NpgsqlConnection _connection;
|
||||
private readonly PostgresConnectionFactory _connection;
|
||||
|
||||
public PostgresUserStore(NpgsqlConnection connection)
|
||||
public PostgresUserStore(PostgresConnectionFactory connection)
|
||||
{
|
||||
_connection = connection;
|
||||
}
|
||||
@ -30,7 +29,8 @@ public class PostgresUserStore : IUserStore
|
||||
/// <inheritdoc />
|
||||
public async ValueTask Set(Guid id, InternalVoidUser obj)
|
||||
{
|
||||
await _connection.ExecuteAsync(
|
||||
await using var conn = await _connection.Get();
|
||||
await conn.ExecuteAsync(
|
||||
@"insert into
|
||||
""Users""(""Id"", ""Email"", ""Password"", ""LastLogin"", ""DisplayName"", ""Avatar"", ""Flags"")
|
||||
values(:id, :email, :password, :lastLogin, :displayName, :avatar, :flags)",
|
||||
@ -48,7 +48,8 @@ values(:id, :email, :password, :lastLogin, :displayName, :avatar, :flags)",
|
||||
{
|
||||
foreach (var r in obj.Roles.Where(a => a != Roles.User))
|
||||
{
|
||||
await _connection.ExecuteAsync(@"insert into ""UserRoles""(""User"", ""Role"") values(:user, :role)",
|
||||
await conn.ExecuteAsync(
|
||||
@"insert into ""UserRoles""(""User"", ""Role"") values(:user, :role)",
|
||||
new {user = obj.Id, role = r});
|
||||
}
|
||||
}
|
||||
@ -57,17 +58,20 @@ values(:id, :email, :password, :lastLogin, :displayName, :avatar, :flags)",
|
||||
/// <inheritdoc />
|
||||
public async ValueTask Delete(Guid id)
|
||||
{
|
||||
await _connection.ExecuteAsync(@"delete from ""Users"" where ""Id"" = :id", new {id});
|
||||
await using var conn = await _connection.Get();
|
||||
await conn.ExecuteAsync(@"delete from ""Users"" where ""Id"" = :id", new {id});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<T?> Get<T>(Guid id) where T : VoidUser
|
||||
{
|
||||
var user = await _connection.QuerySingleOrDefaultAsync<T?>(@"select * from ""Users"" where ""Id"" = :id",
|
||||
await using var conn = await _connection.Get();
|
||||
var user = await conn.QuerySingleOrDefaultAsync<T?>(@"select * from ""Users"" where ""Id"" = :id",
|
||||
new {id});
|
||||
if (user != default)
|
||||
{
|
||||
var roles = await _connection.QueryAsync<string>(@"select ""Role"" from ""UserRoles"" where ""User"" = :id",
|
||||
var roles = await conn.QueryAsync<string>(
|
||||
@"select ""Role"" from ""UserRoles"" where ""User"" = :id",
|
||||
new {id});
|
||||
foreach (var r in roles)
|
||||
{
|
||||
@ -81,7 +85,8 @@ values(:id, :email, :password, :lastLogin, :displayName, :avatar, :flags)",
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<Guid?> LookupUser(string email)
|
||||
{
|
||||
return await _connection.QuerySingleOrDefaultAsync<Guid?>(
|
||||
await using var conn = await _connection.Get();
|
||||
return await conn.QuerySingleOrDefaultAsync<Guid?>(
|
||||
@"select ""Id"" from ""Users"" where ""Email"" = :email",
|
||||
new {email});
|
||||
}
|
||||
@ -89,31 +94,34 @@ values(:id, :email, :password, :lastLogin, :displayName, :avatar, :flags)",
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<PagedResult<PrivateVoidUser>> ListUsers(PagedRequest request)
|
||||
{
|
||||
var orderBy = request.SortBy switch
|
||||
{
|
||||
PagedSortBy.Date => "Created",
|
||||
PagedSortBy.Name => "DisplayName",
|
||||
_ => "Id"
|
||||
};
|
||||
var sortBy = request.SortOrder switch
|
||||
{
|
||||
PageSortOrder.Dsc => "desc",
|
||||
_ => "asc"
|
||||
};
|
||||
var totalUsers = await _connection.ExecuteScalarAsync<int>(@"select count(*) from ""Users""");
|
||||
var users = await _connection.QueryAsync<PrivateVoidUser>(
|
||||
$@"select * from ""Users"" order by ""{orderBy}"" {sortBy} offset :offset limit :limit",
|
||||
new
|
||||
{
|
||||
offset = request.PageSize * request.Page,
|
||||
limit = request.PageSize
|
||||
});
|
||||
await using var conn = await _connection.Get();
|
||||
var totalUsers = await conn.ExecuteScalarAsync<int>(@"select count(*) from ""Users""");
|
||||
|
||||
async IAsyncEnumerable<PrivateVoidUser> Enumerate()
|
||||
{
|
||||
foreach (var u in users ?? Enumerable.Empty<PrivateVoidUser>())
|
||||
var orderBy = request.SortBy switch
|
||||
{
|
||||
yield return u;
|
||||
PagedSortBy.Date => "Created",
|
||||
PagedSortBy.Name => "DisplayName",
|
||||
_ => "Id"
|
||||
};
|
||||
var sortBy = request.SortOrder switch
|
||||
{
|
||||
PageSortOrder.Dsc => "desc",
|
||||
_ => "asc"
|
||||
};
|
||||
await using var iconn = await _connection.Get();
|
||||
var users = await iconn.ExecuteReaderAsync(
|
||||
$@"select * from ""Users"" order by ""{orderBy}"" {sortBy} offset :offset limit :limit",
|
||||
new
|
||||
{
|
||||
offset = request.PageSize * request.Page,
|
||||
limit = request.PageSize
|
||||
});
|
||||
var rowParser = users.GetRowParser<PrivateVoidUser>();
|
||||
while (await users.ReadAsync())
|
||||
{
|
||||
yield return rowParser(users);
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,8 +141,8 @@ values(:id, :email, :password, :lastLogin, :displayName, :avatar, :flags)",
|
||||
if (oldUser == null) return;
|
||||
|
||||
var emailFlag = oldUser.Flags.HasFlag(VoidUserFlags.EmailVerified) ? VoidUserFlags.EmailVerified : 0;
|
||||
|
||||
await _connection.ExecuteAsync(
|
||||
await using var conn = await _connection.Get();
|
||||
await conn.ExecuteAsync(
|
||||
@"update ""Users"" set ""DisplayName"" = @displayName, ""Avatar"" = @avatar, ""Flags"" = :flags where ""Id"" = :id",
|
||||
new
|
||||
{
|
||||
@ -148,7 +156,8 @@ values(:id, :email, :password, :lastLogin, :displayName, :avatar, :flags)",
|
||||
/// <inheritdoc />
|
||||
public async ValueTask UpdateLastLogin(Guid id, DateTime timestamp)
|
||||
{
|
||||
await _connection.ExecuteAsync(@"update ""Users"" set ""LastLogin"" = :timestamp where ""Id"" = :id",
|
||||
await using var conn = await _connection.Get();
|
||||
await conn.ExecuteAsync(@"update ""Users"" set ""LastLogin"" = :timestamp where ""Id"" = :id",
|
||||
new {id, timestamp});
|
||||
}
|
||||
}
|
@ -1,22 +1,20 @@
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Users;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class PostgresUserUploadStore : IUserUploadsStore
|
||||
{
|
||||
private readonly NpgsqlConnection _connection;
|
||||
private readonly IFileInfoManager _fileInfoManager;
|
||||
private readonly PostgresConnectionFactory _connection;
|
||||
|
||||
public PostgresUserUploadStore(NpgsqlConnection connection, IFileInfoManager fileInfoManager)
|
||||
public PostgresUserUploadStore(PostgresConnectionFactory connection)
|
||||
{
|
||||
_connection = connection;
|
||||
_fileInfoManager = fileInfoManager;
|
||||
}
|
||||
|
||||
public async ValueTask<PagedResult<PublicVoidFile>> ListFiles(Guid user, PagedRequest request)
|
||||
public async ValueTask<PagedResult<Guid>> ListFiles(Guid user, PagedRequest request)
|
||||
{
|
||||
var query = @"select {0}
|
||||
from ""UserFiles"" uf, ""Files"" f
|
||||
@ -24,32 +22,31 @@ where uf.""User"" = :user
|
||||
and uf.""File"" = f.""Id""";
|
||||
var queryOrder = @"order by f.""{1}"" {2} limit :limit offset :offset";
|
||||
|
||||
var orderBy = request.SortBy switch
|
||||
{
|
||||
PagedSortBy.Name => "Name",
|
||||
PagedSortBy.Date => "Uploaded",
|
||||
PagedSortBy.Size => "Size",
|
||||
_ => "Id"
|
||||
};
|
||||
var sortOrder = request.SortOrder switch
|
||||
{
|
||||
PageSortOrder.Dsc => "desc",
|
||||
_ => "asc"
|
||||
};
|
||||
var count = await _connection.ExecuteScalarAsync<int>(string.Format(query, "count(*)"), new {user});
|
||||
var files = await _connection.QueryAsync<Guid>(
|
||||
string.Format(query + queryOrder, "uf.\"File\"", orderBy, sortOrder),
|
||||
new {user, offset = request.Page * request.PageSize, limit = request.PageSize});
|
||||
await using var conn = await _connection.Get();
|
||||
var count = await conn.ExecuteScalarAsync<int>(string.Format(query, "count(*)"), new {user});
|
||||
|
||||
async IAsyncEnumerable<PublicVoidFile> EnumerateFiles()
|
||||
async IAsyncEnumerable<Guid> EnumerateFiles()
|
||||
{
|
||||
foreach (var file in files ?? Enumerable.Empty<Guid>())
|
||||
var orderBy = request.SortBy switch
|
||||
{
|
||||
var v = await _fileInfoManager.Get(file);
|
||||
if (v != default)
|
||||
{
|
||||
yield return v;
|
||||
}
|
||||
PagedSortBy.Name => "Name",
|
||||
PagedSortBy.Date => "Uploaded",
|
||||
PagedSortBy.Size => "Size",
|
||||
_ => "Id"
|
||||
};
|
||||
var sortOrder = request.SortOrder switch
|
||||
{
|
||||
PageSortOrder.Dsc => "desc",
|
||||
_ => "asc"
|
||||
};
|
||||
await using var connInner = await _connection.Get();
|
||||
var files = await connInner.ExecuteReaderAsync(
|
||||
string.Format(query + queryOrder, "uf.\"File\"", orderBy, sortOrder),
|
||||
new {user, offset = request.Page * request.PageSize, limit = request.PageSize});
|
||||
var rowParser = files.GetRowParser<Guid>();
|
||||
while (await files.ReadAsync())
|
||||
{
|
||||
yield return rowParser(files);
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,12 +59,22 @@ and uf.""File"" = f.""Id""";
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask AddFile(Guid user, PrivateVoidFile voidFile)
|
||||
{
|
||||
await _connection.ExecuteAsync(@"insert into ""UserFiles""(""File"", ""User"") values(:file, :user)", new
|
||||
await using var conn = await _connection.Get();
|
||||
await conn.ExecuteAsync(@"insert into ""UserFiles""(""File"", ""User"") values(:file, :user)", new
|
||||
{
|
||||
file = voidFile.Id,
|
||||
user
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<Guid?> Uploader(Guid file)
|
||||
{
|
||||
await using var conn = await _connection.Get();
|
||||
return await conn.ExecuteScalarAsync<Guid?>(
|
||||
@"select ""User"" from ""UserFiles"" where ""File"" = :file", new {file});
|
||||
}
|
||||
}
|
@ -3,18 +3,18 @@ using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Users;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class UserUploadStore : IUserUploadsStore
|
||||
{
|
||||
private readonly ICache _cache;
|
||||
private readonly IFileInfoManager _fileInfo;
|
||||
|
||||
public UserUploadStore(ICache cache, IFileInfoManager fileInfo)
|
||||
public UserUploadStore(ICache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
_fileInfo = fileInfo;
|
||||
}
|
||||
|
||||
public async ValueTask<PagedResult<PublicVoidFile>> ListFiles(Guid user, PagedRequest request)
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<PagedResult<Guid>> ListFiles(Guid user, PagedRequest request)
|
||||
{
|
||||
var ids = (await _cache.GetList(MapKey(user))).Select(Guid.Parse);
|
||||
ids = (request.SortBy, request.SortOrder) switch
|
||||
@ -24,15 +24,12 @@ public class UserUploadStore : IUserUploadsStore
|
||||
_ => ids
|
||||
};
|
||||
|
||||
async IAsyncEnumerable<PublicVoidFile> EnumerateResults(IEnumerable<Guid> page)
|
||||
var idsRendered = ids.ToList();
|
||||
async IAsyncEnumerable<Guid> EnumerateResults(IEnumerable<Guid> page)
|
||||
{
|
||||
foreach (var guid in page)
|
||||
foreach (var id in page)
|
||||
{
|
||||
var info = await _fileInfo.Get(guid);
|
||||
if (info != default)
|
||||
{
|
||||
yield return info;
|
||||
}
|
||||
yield return id;
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,15 +37,24 @@ public class UserUploadStore : IUserUploadsStore
|
||||
{
|
||||
Page = request.Page,
|
||||
PageSize = request.PageSize,
|
||||
TotalResults = ids?.Count() ?? 0,
|
||||
Results = EnumerateResults(ids.Skip(request.Page * request.PageSize).Take(request.PageSize))
|
||||
TotalResults = idsRendered.Count,
|
||||
Results = EnumerateResults(idsRendered.Skip(request.Page * request.PageSize).Take(request.PageSize))
|
||||
};
|
||||
}
|
||||
|
||||
public ValueTask AddFile(Guid user, PrivateVoidFile voidFile)
|
||||
/// <inheritdoc />
|
||||
public async ValueTask AddFile(Guid user, PrivateVoidFile voidFile)
|
||||
{
|
||||
return _cache.AddToList(MapKey(user), voidFile.Id.ToString());
|
||||
await _cache.AddToList(MapKey(user), voidFile.Id.ToString());
|
||||
await _cache.Set(MapUploader(voidFile.Id), user);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<Guid?> Uploader(Guid file)
|
||||
{
|
||||
return _cache.Get<Guid?>(MapUploader(file));
|
||||
}
|
||||
|
||||
private static string MapKey(Guid id) => $"user:{id}:uploads";
|
||||
private static string MapUploader(Guid file) => $"file:{file}:uploader";
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user