Delete expired files

This commit is contained in:
Kieran 2022-09-06 22:32:22 +01:00
parent ebe5a0e106
commit 1d451aac82
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
18 changed files with 128 additions and 72 deletions

View File

@ -12,11 +12,11 @@ public class AdminController : Controller
{ {
private readonly FileStoreFactory _fileStore; private readonly FileStoreFactory _fileStore;
private readonly IFileMetadataStore _fileMetadata; private readonly IFileMetadataStore _fileMetadata;
private readonly IFileInfoManager _fileInfo; private readonly FileInfoManager _fileInfo;
private readonly IUserStore _userStore; private readonly IUserStore _userStore;
private readonly IUserUploadsStore _userUploads; private readonly IUserUploadsStore _userUploads;
public AdminController(FileStoreFactory fileStore, IUserStore userStore, IFileInfoManager fileInfo, public AdminController(FileStoreFactory fileStore, IUserStore userStore, FileInfoManager fileInfo,
IFileMetadataStore fileMetadata, IUserUploadsStore userUploads) IFileMetadataStore fileMetadata, IUserUploadsStore userUploads)
{ {
_fileStore = fileStore; _fileStore = fileStore;

View File

@ -11,11 +11,11 @@ namespace VoidCat.Controllers;
public class DownloadController : Controller public class DownloadController : Controller
{ {
private readonly FileStoreFactory _storage; private readonly FileStoreFactory _storage;
private readonly IFileInfoManager _fileInfo; private readonly FileInfoManager _fileInfo;
private readonly IPaywallOrderStore _paywallOrders; private readonly IPaywallOrderStore _paywallOrders;
private readonly ILogger<DownloadController> _logger; private readonly ILogger<DownloadController> _logger;
public DownloadController(FileStoreFactory storage, ILogger<DownloadController> logger, IFileInfoManager fileInfo, public DownloadController(FileStoreFactory storage, ILogger<DownloadController> logger, FileInfoManager fileInfo,
IPaywallOrderStore paywall) IPaywallOrderStore paywall)
{ {
_storage = storage; _storage = storage;

View File

@ -17,14 +17,14 @@ namespace VoidCat.Controllers
private readonly IFileMetadataStore _metadata; private readonly IFileMetadataStore _metadata;
private readonly IPaywallStore _paywall; private readonly IPaywallStore _paywall;
private readonly IPaywallFactory _paywallFactory; private readonly IPaywallFactory _paywallFactory;
private readonly IFileInfoManager _fileInfo; private readonly FileInfoManager _fileInfo;
private readonly IUserUploadsStore _userUploads; private readonly IUserUploadsStore _userUploads;
private readonly IUserStore _userStore; private readonly IUserStore _userStore;
private readonly ITimeSeriesStatsReporter _timeSeriesStats; private readonly ITimeSeriesStatsReporter _timeSeriesStats;
private readonly VoidSettings _settings; private readonly VoidSettings _settings;
public UploadController(FileStoreFactory storage, IFileMetadataStore metadata, IPaywallStore paywall, public UploadController(FileStoreFactory storage, IFileMetadataStore metadata, IPaywallStore paywall,
IPaywallFactory paywallFactory, IFileInfoManager fileInfo, IUserUploadsStore userUploads, IPaywallFactory paywallFactory, FileInfoManager fileInfo, IUserUploadsStore userUploads,
ITimeSeriesStatsReporter timeSeriesStats, IUserStore userStore, VoidSettings settings) ITimeSeriesStatsReporter timeSeriesStats, IUserStore userStore, VoidSettings settings)
{ {
_storage = storage; _storage = storage;

View File

@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using VoidCat.Model; using VoidCat.Model;
using VoidCat.Services.Abstractions; using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
namespace VoidCat.Controllers; namespace VoidCat.Controllers;
@ -10,10 +11,10 @@ public class UserController : Controller
private readonly IUserStore _store; private readonly IUserStore _store;
private readonly IUserUploadsStore _userUploads; private readonly IUserUploadsStore _userUploads;
private readonly IEmailVerification _emailVerification; private readonly IEmailVerification _emailVerification;
private readonly IFileInfoManager _fileInfoManager; private readonly FileInfoManager _fileInfoManager;
public UserController(IUserStore store, IUserUploadsStore userUploads, IEmailVerification emailVerification, public UserController(IUserStore store, IUserUploadsStore userUploads, IEmailVerification emailVerification,
IFileInfoManager fileInfoManager) FileInfoManager fileInfoManager)
{ {
_store = store; _store = store;
_userUploads = userUploads; _userUploads = userUploads;

View File

@ -1,38 +0,0 @@
using VoidCat.Model;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// Main interface for getting file info to serve to clients.
/// This interface should wrap all stores and return the combined result
/// </summary>
public interface IFileInfoManager
{
/// <summary>
/// Get all metadata for a single file
/// </summary>
/// <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
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
ValueTask<IReadOnlyList<PublicVoidFile>> Get(Guid[] ids);
/// <summary>
/// Deletes all file metadata
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask Delete(Guid id);
}

View File

@ -0,0 +1,52 @@
using VoidCat.Model;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
namespace VoidCat.Services.Background;
/// <summary>
/// Delete expired files
/// </summary>
public sealed class DeleteExpiredFiles : BackgroundService
{
private readonly ILogger<DeleteExpiredFiles> _logger;
private readonly IServiceScopeFactory _scopeFactory;
public DeleteExpiredFiles(ILogger<DeleteExpiredFiles> logger, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = _scopeFactory.CreateScope();
var metadata = scope.ServiceProvider.GetRequiredService<IFileMetadataStore>();
var fileInfoManager = scope.ServiceProvider.GetRequiredService<FileInfoManager>();
var fileStoreFactory = scope.ServiceProvider.GetRequiredService<FileStoreFactory>();
var files = await metadata.ListFiles<SecretVoidFileMeta>(new(0, int.MaxValue));
await foreach (var f in files.Results.WithCancellation(stoppingToken))
{
try
{
if (f.Expires < DateTime.Now)
{
await fileStoreFactory.DeleteFile(f.Id);
await fileInfoManager.Delete(f.Id);
_logger.LogInformation("Deleted file: {Id}", f.Id);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete file: {Id}", f.Id);
}
}
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}

View File

@ -25,7 +25,7 @@ public class DeleteUnverifiedAccounts : BackgroundService
var userStore = scope.ServiceProvider.GetRequiredService<IUserStore>(); var userStore = scope.ServiceProvider.GetRequiredService<IUserStore>();
var userUploads = scope.ServiceProvider.GetRequiredService<IUserUploadsStore>(); var userUploads = scope.ServiceProvider.GetRequiredService<IUserUploadsStore>();
var fileStore = scope.ServiceProvider.GetRequiredService<FileStoreFactory>(); var fileStore = scope.ServiceProvider.GetRequiredService<FileStoreFactory>();
var fileInfoManager = scope.ServiceProvider.GetRequiredService<IFileInfoManager>(); var fileInfoManager = scope.ServiceProvider.GetRequiredService<FileInfoManager>();
var accounts = await userStore.ListUsers(new(0, Int32.MaxValue)); var accounts = await userStore.ListUsers(new(0, Int32.MaxValue));

View File

@ -3,8 +3,11 @@ using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Files; namespace VoidCat.Services.Files;
/// <inheritdoc /> /// <summary>
public class FileInfoManager : IFileInfoManager /// Main interface for getting file info to serve to clients.
/// This interface should wrap all stores and return the combined result
/// </summary>
public sealed class FileInfoManager
{ {
private readonly IFileMetadataStore _metadataStore; private readonly IFileMetadataStore _metadataStore;
private readonly IPaywallStore _paywallStore; private readonly IPaywallStore _paywallStore;
@ -24,19 +27,31 @@ public class FileInfoManager : IFileInfoManager
_userUploadsStore = userUploadsStore; _userUploadsStore = userUploadsStore;
} }
/// <inheritdoc /> /// <summary>
/// Get all metadata for a single file
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public ValueTask<PublicVoidFile?> Get(Guid id) public ValueTask<PublicVoidFile?> Get(Guid id)
{ {
return Get<PublicVoidFile, VoidFileMeta>(id); return Get<PublicVoidFile, VoidFileMeta>(id);
} }
/// <inheritdoc /> /// <summary>
/// Get all private metadata for a single file
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public ValueTask<PrivateVoidFile?> GetPrivate(Guid id) public ValueTask<PrivateVoidFile?> GetPrivate(Guid id)
{ {
return Get<PrivateVoidFile, SecretVoidFileMeta>(id); return Get<PrivateVoidFile, SecretVoidFileMeta>(id);
} }
/// <inheritdoc /> /// <summary>
/// Get all metadata for multiple files
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
public async ValueTask<IReadOnlyList<PublicVoidFile>> Get(Guid[] ids) public async ValueTask<IReadOnlyList<PublicVoidFile>> Get(Guid[] ids)
{ {
var ret = new List<PublicVoidFile>(); var ret = new List<PublicVoidFile>();
@ -52,7 +67,11 @@ public class FileInfoManager : IFileInfoManager
return ret; return ret;
} }
/// <inheritdoc /> /// <summary>
/// Deletes all file metadata
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async ValueTask Delete(Guid id) public async ValueTask Delete(Guid id)
{ {
await _metadataStore.Delete(id); await _metadataStore.Delete(id);

View File

@ -8,7 +8,7 @@ public static class FileStorageStartup
{ {
public static void AddStorage(this IServiceCollection services, VoidSettings settings) public static void AddStorage(this IServiceCollection services, VoidSettings settings)
{ {
services.AddTransient<IFileInfoManager, FileInfoManager>(); services.AddTransient<FileInfoManager>();
services.AddTransient<FileStoreFactory>(); services.AddTransient<FileStoreFactory>();
if (settings.CloudStorage != default) if (settings.CloudStorage != default)
@ -19,7 +19,7 @@ public static class FileStorageStartup
services.AddTransient<IFileStore>((svc) => services.AddTransient<IFileStore>((svc) =>
new S3FileStore(s3, new S3FileStore(s3,
svc.GetRequiredService<IAggregateStatsCollector>(), svc.GetRequiredService<IAggregateStatsCollector>(),
svc.GetRequiredService<IFileInfoManager>(), svc.GetRequiredService<FileInfoManager>(),
svc.GetRequiredService<ICache>())); svc.GetRequiredService<ICache>()));
if (settings.MetadataStore == s3.Name) if (settings.MetadataStore == s3.Name)

View File

@ -52,8 +52,8 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
oldMeta.Description = meta.Description ?? oldMeta.Description; oldMeta.Description = meta.Description ?? oldMeta.Description;
oldMeta.Name = meta.Name ?? oldMeta.Name; oldMeta.Name = meta.Name ?? oldMeta.Name;
oldMeta.MimeType = meta.MimeType ?? oldMeta.MimeType; oldMeta.MimeType = meta.MimeType ?? oldMeta.MimeType;
oldMeta.Expires = meta.Expires ?? oldMeta.Expires;
oldMeta.Storage = meta.Storage ?? oldMeta.Storage; oldMeta.Storage = meta.Storage ?? oldMeta.Storage;
oldMeta.Expires = meta.Expires;
await Set(id, oldMeta); await Set(id, oldMeta);
} }

View File

@ -8,14 +8,12 @@ namespace VoidCat.Services.Files;
public class LocalDiskFileStore : StreamFileStore, IFileStore public class LocalDiskFileStore : StreamFileStore, IFileStore
{ {
private const string FilesDir = "files-v1"; private const string FilesDir = "files-v1";
private readonly ILogger<LocalDiskFileStore> _logger;
private readonly VoidSettings _settings; private readonly VoidSettings _settings;
public LocalDiskFileStore(ILogger<LocalDiskFileStore> logger, VoidSettings settings, IAggregateStatsCollector stats) public LocalDiskFileStore(VoidSettings settings, IAggregateStatsCollector stats)
: base(stats) : base(stats)
{ {
_settings = settings; _settings = settings;
_logger = logger;
var dir = Path.Combine(_settings.DataDirectory, FilesDir); var dir = Path.Combine(_settings.DataDirectory, FilesDir);
if (!Directory.Exists(dir)) if (!Directory.Exists(dir))
@ -55,7 +53,6 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
var fp = MapPath(id); var fp = MapPath(id);
if (File.Exists(fp)) if (File.Exists(fp))
{ {
_logger.LogInformation("Deleting file: {Path}", fp);
File.Delete(fp); File.Delete(fp);
} }

View File

@ -54,7 +54,7 @@ on conflict (""Id"") do update set
mimeType = obj.MimeType, mimeType = obj.MimeType,
digest = obj.Digest, digest = obj.Digest,
editSecret = obj.EditSecret, editSecret = obj.EditSecret,
expires = obj.Expires, expires = obj.Expires?.ToUniversalTime(),
store = obj.Storage store = obj.Storage
}); });
} }
@ -91,8 +91,8 @@ on conflict (""Id"") do update set
oldMeta.Description = meta.Description ?? oldMeta.Description; oldMeta.Description = meta.Description ?? oldMeta.Description;
oldMeta.Name = meta.Name ?? oldMeta.Name; oldMeta.Name = meta.Name ?? oldMeta.Name;
oldMeta.MimeType = meta.MimeType ?? oldMeta.MimeType; oldMeta.MimeType = meta.MimeType ?? oldMeta.MimeType;
oldMeta.Expires = meta.Expires ?? oldMeta.Expires;
oldMeta.Storage = meta.Storage ?? oldMeta.Storage; oldMeta.Storage = meta.Storage ?? oldMeta.Storage;
oldMeta.Expires = meta.Expires;
await Set(id, oldMeta); await Set(id, oldMeta);
} }

View File

@ -53,8 +53,8 @@ public class S3FileMetadataStore : IFileMetadataStore
oldMeta.Description = meta.Description ?? oldMeta.Description; oldMeta.Description = meta.Description ?? oldMeta.Description;
oldMeta.Name = meta.Name ?? oldMeta.Name; oldMeta.Name = meta.Name ?? oldMeta.Name;
oldMeta.MimeType = meta.MimeType ?? oldMeta.MimeType; oldMeta.MimeType = meta.MimeType ?? oldMeta.MimeType;
oldMeta.Expires = meta.Expires ?? oldMeta.Expires;
oldMeta.Storage = meta.Storage ?? oldMeta.Storage; oldMeta.Storage = meta.Storage ?? oldMeta.Storage;
oldMeta.Expires = meta.Expires;
await Set(id, oldMeta); await Set(id, oldMeta);
} }

View File

@ -9,13 +9,13 @@ namespace VoidCat.Services.Files;
/// <inheritdoc cref="VoidCat.Services.Abstractions.IFileStore" /> /// <inheritdoc cref="VoidCat.Services.Abstractions.IFileStore" />
public class S3FileStore : StreamFileStore, IFileStore public class S3FileStore : StreamFileStore, IFileStore
{ {
private readonly IFileInfoManager _fileInfo; private readonly FileInfoManager _fileInfo;
private readonly AmazonS3Client _client; private readonly AmazonS3Client _client;
private readonly S3BlobConfig _config; private readonly S3BlobConfig _config;
private readonly IAggregateStatsCollector _statsCollector; private readonly IAggregateStatsCollector _statsCollector;
private readonly ICache _cache; private readonly ICache _cache;
public S3FileStore(S3BlobConfig settings, IAggregateStatsCollector stats, IFileInfoManager fileInfo, ICache cache) : base(stats) public S3FileStore(S3BlobConfig settings, IAggregateStatsCollector stats, FileInfoManager fileInfo, ICache cache) : base(stats)
{ {
_fileInfo = fileInfo; _fileInfo = fileInfo;
_cache = cache; _cache = cache;

View File

@ -155,6 +155,7 @@ public static class VoidStartup
public static void AddBackgroundServices(this IServiceCollection services, VoidSettings voidSettings) public static void AddBackgroundServices(this IServiceCollection services, VoidSettings voidSettings)
{ {
services.AddHostedService<DeleteUnverifiedAccounts>(); services.AddHostedService<DeleteUnverifiedAccounts>();
services.AddHostedService<DeleteExpiredFiles>();
if (voidSettings.HasVirusScanner()) if (voidSettings.HasVirusScanner())
{ {

View File

@ -23,7 +23,11 @@ export default function ApiKeyList() {
setNewApiKey(await rsp.json()); setNewApiKey(await rsp.json());
} }
} }
function openDocs() {
window.open("/swagger", "_blank")
}
useEffect(() => { useEffect(() => {
if (Api) { if (Api) {
loadApiKeys(); loadApiKeys();
@ -38,6 +42,7 @@ export default function ApiKeyList() {
</div> </div>
<div> <div>
<VoidButton onClick={(e) => createApiKey()}>+New</VoidButton> <VoidButton onClick={(e) => createApiKey()}>+New</VoidButton>
<VoidButton onClick={(e) => openDocs()}>Docs</VoidButton>
</div> </div>
</div> </div>
<table> <table>

View File

@ -6,6 +6,7 @@ import {useApi} from "./Api";
import "./FileEdit.css"; import "./FileEdit.css";
import {useSelector} from "react-redux"; import {useSelector} from "react-redux";
import {VoidButton} from "./VoidButton"; import {VoidButton} from "./VoidButton";
import moment from "moment";
export function FileEdit(props) { export function FileEdit(props) {
const {Api} = useApi(); const {Api} = useApi();
@ -15,9 +16,10 @@ export function FileEdit(props) {
const [paywall, setPaywall] = useState(file.paywall?.service); const [paywall, setPaywall] = useState(file.paywall?.service);
const [name, setName] = useState(meta?.name); const [name, setName] = useState(meta?.name);
const [description, setDescription] = useState(meta?.description); const [description, setDescription] = useState(meta?.description);
const [expiry, setExpiry] = useState(meta?.expires === undefined || meta?.expires === null ? null : moment(meta?.expires).unix() * 1000);
const privateFile = file?.uploader?.id && profile?.id === file.uploader.id const privateFile = file?.uploader?.id && profile?.id === file.uploader.id
? file ? file
: JSON.parse(window.localStorage.getItem(file.id)); : JSON.parse(window.localStorage.getItem(file.id));
if (!privateFile || privateFile?.metadata?.editSecret === null) { if (!privateFile || privateFile?.metadata?.editSecret === null) {
return null; return null;
@ -32,11 +34,12 @@ export function FileEdit(props) {
let meta = { let meta = {
name, name,
description, description,
editSecret: privateFile?.metadata?.editSecret editSecret: privateFile?.metadata?.editSecret,
expires: moment(expiry).toISOString()
}; };
await Api.updateMetadata(file.id, meta); await Api.updateMetadata(file.id, meta);
} }
function renderPaywallConfig() { function renderPaywallConfig() {
switch (paywall) { switch (paywall) {
case 0: { case 0: {
@ -58,6 +61,22 @@ export function FileEdit(props) {
<dd><input type="text" value={name} onChange={(e) => setName(e.target.value)}/></dd> <dd><input type="text" value={name} onChange={(e) => setName(e.target.value)}/></dd>
<dt>Description:</dt> <dt>Description:</dt>
<dd><input type="text" value={description} onChange={(e) => setDescription(e.target.value)}/></dd> <dd><input type="text" value={description} onChange={(e) => setDescription(e.target.value)}/></dd>
<dt>Expiry</dt>
<dd>
<input type="datetime-local"
value={expiry === null ? "" : moment(expiry).toISOString().replace("Z", "")}
max={moment.utc().add(1, "year").toISOString().replace("Z", "")}
min={moment.utc().toISOString().replace("Z", "")}
onChange={(e) => {
console.log(e.target.value);
if (e.target.value.length > 0) {
setExpiry(moment.utc(e.target.value).unix() * 1000);
} else {
setExpiry(null);
}
}}/>
</dd>
</dl> </dl>
<VoidButton onClick={(e) => saveMeta()} options={{showSuccess: true}}>Save</VoidButton> <VoidButton onClick={(e) => saveMeta()} options={{showSuccess: true}}>Save</VoidButton>
</div> </div>

View File

@ -60,7 +60,7 @@ a:hover {
align-items: center; align-items: center;
} }
input[type="text"], input[type="number"], input[type="password"], select { input[type="text"], input[type="number"], input[type="password"], input[type="datetime-local"], select {
display: inline-block; display: inline-block;
line-height: 1.1; line-height: 1.1;
border-radius: 10px; border-radius: 10px;