Decouple user upload store

This commit is contained in:
Kieran 2022-06-10 21:42:36 +01:00
parent 1f484f243d
commit cba4d5fc80
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
16 changed files with 246 additions and 170 deletions

View File

@ -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();

View File

@ -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>

View File

@ -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)

View File

@ -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>

View File

@ -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();

View File

@ -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

View File

@ -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);
}

View 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);
}
}
}

View 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
};
}
}

View File

@ -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);
}

View File

@ -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();
}
}
}

View 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;
}
}

View File

@ -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});
}
}

View File

@ -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});
}
}

View File

@ -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});
}
}

View 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";
}