Refactor basic store

This commit is contained in:
Kieran 2022-03-07 13:38:53 +00:00
parent 5d07cc93eb
commit 156cec9828
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
19 changed files with 179 additions and 74 deletions

View File

@ -28,7 +28,9 @@ public class UserController : Controller
var requestedId = isMe ? loggedUser!.Value : id.FromBase58Guid(); var requestedId = isMe ? loggedUser!.Value : id.FromBase58Guid();
if (loggedUser == requestedId) if (loggedUser == requestedId)
{ {
return Json(await _store.Get<PrivateVoidUser>(requestedId)); var pUser = await _store.Get<PrivateVoidUser>(requestedId);
if (pUser == default) return NotFound();
return Json(pUser);
} }
var user = await _store.Get<PublicVoidUser>(requestedId); var user = await _store.Get<PublicVoidUser>(requestedId);
@ -94,7 +96,7 @@ public class UserController : Controller
if (!await _emailVerification.VerifyCode(user, token)) return BadRequest(); if (!await _emailVerification.VerifyCode(user, token)) return BadRequest();
user.Flags |= VoidUserFlags.EmailVerified; user.Flags |= VoidUserFlags.EmailVerified;
await _store.Set(user); await _store.Set(user.Id, user);
return Accepted(); return Accepted();
} }

View File

@ -15,6 +15,7 @@ using VoidCat.Services.Paywall;
using VoidCat.Services.Redis; using VoidCat.Services.Redis;
using VoidCat.Services.Stats; using VoidCat.Services.Stats;
using VoidCat.Services.Users; using VoidCat.Services.Users;
using VoidCat.Services.VirusScanner;
// setup JsonConvert default settings // setup JsonConvert default settings
JsonSerializerSettings ConfigJsonSettings(JsonSerializerSettings s) JsonSerializerSettings ConfigJsonSettings(JsonSerializerSettings s)
@ -99,6 +100,9 @@ services.AddTransient<IEmailVerification, EmailVerification>();
// background services // background services
services.AddHostedService<DeleteUnverifiedAccounts>(); services.AddHostedService<DeleteUnverifiedAccounts>();
// virus scanner
services.AddVirusScanner(voidSettings);
if (useRedis) if (useRedis)
{ {
services.AddTransient<ICache, RedisCache>(); services.AddTransient<ICache, RedisCache>();

View File

@ -0,0 +1,12 @@
namespace VoidCat.Services.Abstractions;
public interface IBasicStore<T>
{
ValueTask<T?> Get(Guid id);
ValueTask Set(Guid id, T obj);
ValueTask Delete(Guid id);
string MapKey(Guid id);
}

View File

@ -2,11 +2,7 @@ using VoidCat.Model;
namespace VoidCat.Services.Abstractions; namespace VoidCat.Services.Abstractions;
public interface IFileMetadataStore public interface IFileMetadataStore : IPublicPrivateStore<VoidFileMeta, SecretVoidFileMeta>
{ {
ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : VoidFileMeta; ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : VoidFileMeta;
ValueTask Set(Guid id, SecretVoidFileMeta meta);
ValueTask Delete(Guid id);
} }

View File

@ -11,4 +11,6 @@ public interface IFileStore
ValueTask<PagedResult<PublicVoidFile>> ListFiles(PagedRequest request); ValueTask<PagedResult<PublicVoidFile>> ListFiles(PagedRequest request);
ValueTask DeleteFile(Guid id); ValueTask DeleteFile(Guid id);
ValueTask<Stream> Open(EgressRequest request, CancellationToken cts);
} }

View File

@ -2,12 +2,8 @@ using VoidCat.Model.Paywall;
namespace VoidCat.Services.Abstractions; namespace VoidCat.Services.Abstractions;
public interface IPaywallStore public interface IPaywallStore : IBasicStore<PaywallConfig>
{ {
ValueTask<PaywallOrder?> GetOrder(Guid id); ValueTask<PaywallOrder?> GetOrder(Guid id);
ValueTask SaveOrder(PaywallOrder order); ValueTask SaveOrder(PaywallOrder order);
ValueTask<PaywallConfig?> Get(Guid id);
ValueTask Set(Guid id, PaywallConfig config);
ValueTask Delete(Guid id);
} }

View File

@ -0,0 +1,10 @@
namespace VoidCat.Services.Abstractions;
public interface IPublicPrivateStore<TPublic, in TPrivate>
{
ValueTask<TPublic?> Get(Guid id);
ValueTask Set(Guid id, TPrivate obj);
ValueTask Delete(Guid id);
}

View File

@ -2,12 +2,12 @@
namespace VoidCat.Services.Abstractions; namespace VoidCat.Services.Abstractions;
public interface IUserStore public interface IUserStore : IPublicPrivateStore<VoidUser, InternalVoidUser>
{ {
ValueTask<Guid?> LookupUser(string email);
ValueTask<T?> Get<T>(Guid id) where T : VoidUser; ValueTask<T?> Get<T>(Guid id) where T : VoidUser;
ValueTask Set(InternalVoidUser user); ValueTask Delete(PrivateVoidUser user);
ValueTask<Guid?> LookupUser(string email);
ValueTask<PagedResult<PrivateVoidUser>> ListUsers(PagedRequest request); ValueTask<PagedResult<PrivateVoidUser>> ListUsers(PagedRequest request);
ValueTask UpdateProfile(PublicVoidUser newUser); ValueTask UpdateProfile(PublicVoidUser newUser);
ValueTask Delete(PrivateVoidUser user);
} }

View File

@ -34,6 +34,7 @@ public class DeleteUnverifiedAccounts : BackgroundService
await _userStore.Delete(account); await _userStore.Delete(account);
var files = await _userUploads.ListFiles(account.Id, new(0, Int32.MinValue)); var files = await _userUploads.ListFiles(account.Id, new(0, Int32.MinValue));
// ReSharper disable once UseCancellationTokenForIAsyncEnumerable
await foreach (var file in files.Results) await foreach (var file in files.Results)
{ {
await _fileStore.DeleteFile(file.Id); await _fileStore.DeleteFile(file.Id);

View File

@ -0,0 +1,30 @@
using VoidCat.Services.Abstractions;
namespace VoidCat.Services;
public abstract class BasicCacheStore<TStore> : IBasicStore<TStore>
{
protected readonly ICache _cache;
protected BasicCacheStore(ICache cache)
{
_cache = cache;
}
public virtual ValueTask<TStore?> Get(Guid id)
{
return _cache.Get<TStore>(MapKey(id));
}
public virtual ValueTask Set(Guid id, TStore obj)
{
return _cache.Set(MapKey(id), obj);
}
public virtual ValueTask Delete(Guid id)
{
return _cache.Delete(MapKey(id));
}
public abstract string MapKey(Guid id);
}

View File

@ -28,6 +28,11 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
return GetMeta<TMeta>(id); return GetMeta<TMeta>(id);
} }
public ValueTask<VoidFileMeta?> Get(Guid id)
{
return GetMeta<VoidFileMeta>(id);
}
public async ValueTask Set(Guid id, SecretVoidFileMeta meta) public async ValueTask Set(Guid id, SecretVoidFileMeta meta)
{ {
var path = MapMeta(id); var path = MapMeta(id);

View File

@ -30,10 +30,7 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
public async ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts) public async ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts)
{ {
var path = MapPath(request.Id); await using var fs = await Open(request, cts);
if (!File.Exists(path)) throw new VoidFileNotFoundException(request.Id);
await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
await EgressFromStream(fs, request, outStream, cts); await EgressFromStream(fs, request, outStream, cts);
} }
@ -100,6 +97,14 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
await _metadataStore.Delete(id); await _metadataStore.Delete(id);
} }
public ValueTask<Stream> Open(EgressRequest request, CancellationToken cts)
{
var path = MapPath(request.Id);
if (!File.Exists(path)) throw new VoidFileNotFoundException(request.Id);
return ValueTask.FromResult<Stream>(new FileStream(path, FileMode.Open, FileAccess.Read));
}
private string MapPath(Guid id) => private string MapPath(Guid id) =>
Path.Join(_settings.DataDirectory, FilesDir, id.ToString()); Path.Join(_settings.DataDirectory, FilesDir, id.ToString());
} }

View File

@ -20,7 +20,33 @@ public class S3FileMetadataStore : IFileMetadataStore
_client = _config.CreateClient(); _client = _config.CreateClient();
} }
public async ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : VoidFileMeta public ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : VoidFileMeta
{
return GetMeta<TMeta>(id);
}
public ValueTask<VoidFileMeta?> Get(Guid id)
{
return GetMeta<VoidFileMeta>(id);
}
public async ValueTask Set(Guid id, SecretVoidFileMeta meta)
{
await _client.PutObjectAsync(new()
{
BucketName = _config.BucketName,
Key = ToKey(id),
ContentBody = JsonConvert.SerializeObject(meta),
ContentType = "application/json"
});
}
public async ValueTask Delete(Guid id)
{
await _client.DeleteObjectAsync(_config.BucketName, ToKey(id));
}
private async ValueTask<TMeta?> GetMeta<TMeta>(Guid id) where TMeta : VoidFileMeta
{ {
try try
{ {
@ -48,22 +74,6 @@ public class S3FileMetadataStore : IFileMetadataStore
return default; return default;
} }
public async ValueTask Set(Guid id, SecretVoidFileMeta meta)
{
await _client.PutObjectAsync(new()
{
BucketName = _config.BucketName,
Key = ToKey(id),
ContentBody = JsonConvert.SerializeObject(meta),
ContentType = "application/json"
});
}
public async ValueTask Delete(Guid id)
{
await _client.DeleteObjectAsync(_config.BucketName, ToKey(id));
}
private static string ToKey(Guid id) => $"{id}-metadata"; private static string ToKey(Guid id) => $"{id}-metadata";
} }

View File

@ -40,7 +40,7 @@ public class S3FileStore : StreamFileStore, IFileStore
}, },
Headers = Headers =
{ {
ContentLength = (long)payload.Meta.Size ContentLength = (long) payload.Meta.Size
} }
}; };
@ -50,19 +50,8 @@ public class S3FileStore : StreamFileStore, IFileStore
public async ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts) public async ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts)
{ {
var req = new GetObjectRequest() await using var stream = await Open(request, cts);
{ await EgressFull(request.Id, stream, outStream, cts);
BucketName = _config.BucketName,
Key = request.Id.ToString()
};
if (request.Ranges.Any())
{
var r = request.Ranges.First();
req.ByteRange = new ByteRange(r.OriginalString);
}
var obj = await _client.GetObjectAsync(req, cts);
await EgressFull(request.Id, obj.ResponseStream, outStream, cts);
} }
public async ValueTask<PagedResult<PublicVoidFile>> ListFiles(PagedRequest request) public async ValueTask<PagedResult<PublicVoidFile>> ListFiles(PagedRequest request)
@ -124,4 +113,21 @@ public class S3FileStore : StreamFileStore, IFileStore
{ {
await _client.DeleteObjectAsync(_config.BucketName, id.ToString()); await _client.DeleteObjectAsync(_config.BucketName, id.ToString());
} }
public async ValueTask<Stream> Open(EgressRequest request, CancellationToken cts)
{
var req = new GetObjectRequest()
{
BucketName = _config.BucketName,
Key = request.Id.ToString()
};
if (request.Ranges.Any())
{
var r = request.Ranges.First();
req.ByteRange = new ByteRange(r.OriginalString);
}
var obj = await _client.GetObjectAsync(req, cts);
return obj.ResponseStream;
}
} }

View File

@ -3,36 +3,24 @@ using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Paywall; namespace VoidCat.Services.Paywall;
public class PaywallStore : IPaywallStore public class PaywallStore : BasicCacheStore<PaywallConfig>, IPaywallStore
{ {
private readonly ICache _cache;
public PaywallStore(ICache database) public PaywallStore(ICache database)
: base(database)
{ {
_cache = database;
} }
public async ValueTask<PaywallConfig?> Get(Guid id) public override async ValueTask<PaywallConfig?> Get(Guid id)
{ {
var cfg = await _cache.Get<NoPaywallConfig>(ConfigKey(id)); var cfg = await _cache.Get<NoPaywallConfig>(MapKey(id));
return cfg?.Service switch return cfg?.Service switch
{ {
PaywallServices.None => cfg, PaywallServices.None => cfg,
PaywallServices.Strike => await _cache.Get<StrikePaywallConfig>(ConfigKey(id)), PaywallServices.Strike => await _cache.Get<StrikePaywallConfig>(MapKey(id)),
_ => default _ => default
}; };
} }
public ValueTask Set(Guid id, PaywallConfig config)
{
return _cache.Set(ConfigKey(id), config);
}
public ValueTask Delete(Guid id)
{
return _cache.Delete(ConfigKey(id));
}
public async ValueTask<PaywallOrder?> GetOrder(Guid id) public async ValueTask<PaywallOrder?> GetOrder(Guid id)
{ {
return await _cache.Get<PaywallOrder>(OrderKey(id)); return await _cache.Get<PaywallOrder>(OrderKey(id));
@ -44,6 +32,6 @@ public class PaywallStore : IPaywallStore
order.Status == PaywallOrderStatus.Paid ? TimeSpan.FromDays(1) : TimeSpan.FromSeconds(5)); order.Status == PaywallOrderStatus.Paid ? TimeSpan.FromDays(1) : TimeSpan.FromSeconds(5));
} }
private string ConfigKey(Guid id) => $"paywall:config:{id}"; public override string MapKey(Guid id) => $"paywall:config:{id}";
private string OrderKey(Guid id) => $"paywall:order:{id}"; private string OrderKey(Guid id) => $"paywall:order:{id}";
} }

View File

@ -24,7 +24,7 @@ public class UserManager : IUserManager
if (!(user?.CheckPassword(password) ?? false)) throw new InvalidOperationException("User does not exist"); if (!(user?.CheckPassword(password) ?? false)) throw new InvalidOperationException("User does not exist");
user.LastLogin = DateTimeOffset.UtcNow; user.LastLogin = DateTimeOffset.UtcNow;
await _store.Set(user); await _store.Set(user.Id, user);
return user; return user;
} }
@ -51,7 +51,7 @@ public class UserManager : IUserManager
} }
} }
await _store.Set(newUser); await _store.Set(newUser.Id, newUser);
await _emailVerification.SendNewCode(newUser); await _emailVerification.SendNewCode(newUser);
return newUser; return newUser;
} }

View File

@ -20,6 +20,11 @@ public class UserStore : IUserStore
return await _cache.Get<Guid>(MapKey(email)); return await _cache.Get<Guid>(MapKey(email));
} }
public async ValueTask<VoidUser?> Get(Guid id)
{
return await Get<PublicVoidUser>(id);
}
public async ValueTask<T?> Get<T>(Guid id) where T : VoidUser public async ValueTask<T?> Get<T>(Guid id) where T : VoidUser
{ {
try try
@ -34,8 +39,10 @@ public class UserStore : IUserStore
return default; return default;
} }
public async ValueTask Set(InternalVoidUser user) public async ValueTask Set(Guid id, InternalVoidUser user)
{ {
if (id != user.Id) throw new InvalidOperationException();
await _cache.Set(MapKey(user.Id), user); await _cache.Set(MapKey(user.Id), user);
await _cache.AddToList(UserList, user.Id.ToString()); await _cache.AddToList(UserList, user.Id.ToString());
await _cache.Set(MapKey(user.Email), user.Id.ToString()); await _cache.Set(MapKey(user.Email), user.Id.ToString());
@ -84,7 +91,14 @@ public class UserStore : IUserStore
oldUser.Flags = newUser.Flags; oldUser.Flags = newUser.Flags;
oldUser.DisplayName = newUser.DisplayName; oldUser.DisplayName = newUser.DisplayName;
await Set(oldUser); await Set(newUser.Id, oldUser);
}
public async ValueTask Delete(Guid id)
{
var user = await Get<InternalVoidUser>(id);
if (user == default) throw new InvalidOperationException();
await Delete(user);
} }
public async ValueTask Delete(PrivateVoidUser user) public async ValueTask Delete(PrivateVoidUser user)

View File

@ -19,4 +19,10 @@
.preview .file-stats svg { .preview .file-stats svg {
vertical-align: middle; vertical-align: middle;
margin-right: 10px; margin-right: 10px;
}
.preview .virus-warning {
padding: 10px;
border-radius: 10px;
border: 1px solid red;
} }

View File

@ -108,6 +108,23 @@ export function FilePreview() {
return tags; return tags;
} }
function renderVirusWarning() {
if(info.virusScan && info.virusScan.isVirus === true) {
let scanResult = info.virusScan;
return (
<div className="virus-warning">
<p>
This file apears to be a virus, take care when downloading this file.
</p>
Detected as:
<pre>
{scanResult.virusNames.join('\n')}
</pre>
</div>
);
}
}
useEffect(() => { useEffect(() => {
loadInfo(); loadInfo();
}, []); }, []);
@ -135,6 +152,7 @@ export function FilePreview() {
{info.metadata?.description ? <meta name="description" content={info.metadata?.description}/> : null} {info.metadata?.description ? <meta name="description" content={info.metadata?.description}/> : null}
{renderOpenGraphTags()} {renderOpenGraphTags()}
</Helmet> </Helmet>
{renderVirusWarning()}
<div className="flex flex-center"> <div className="flex flex-center">
<div className="flx-grow"> <div className="flx-grow">
{info.uploader ? <InlineProfile profile={info.uploader}/> : null} {info.uploader ? <InlineProfile profile={info.uploader}/> : null}