FilesV2 + Cleanup
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Kieran 2023-08-24 10:51:07 +01:00
parent b4feae365a
commit e51672352f
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
9 changed files with 195 additions and 27 deletions

View File

@ -12,6 +12,13 @@ public interface IFileStore
/// Return key for named instance
/// </summary>
string? Key { get; }
/// <summary>
/// Check if a file exists in the store
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask<bool> Exists(Guid id);
/// <summary>
/// Ingress a file into the system (Upload)

View File

@ -35,6 +35,12 @@ public class FileStoreFactory : IFileStore
/// <inheritdoc />
public string? Key => null;
public async ValueTask<bool> Exists(Guid id)
{
var store = await GetStore(id);
return await store.Exists(id);
}
/// <inheritdoc />
public ValueTask<Database.File> Ingress(IngressPayload payload, CancellationToken cts)
{
@ -43,7 +49,7 @@ public class FileStoreFactory : IFileStore
{
throw new InvalidOperationException($"Cannot find store '{payload.Meta.Storage}'");
}
return store.Ingress(payload, cts);
}
@ -53,7 +59,7 @@ public class FileStoreFactory : IFileStore
var store = await GetStore(request.Id);
await store.Egress(request, outStream, cts);
}
/// <inheritdoc />
public async ValueTask<EgressResult> StartEgress(EgressRequest request)
{

View File

@ -8,7 +8,6 @@ namespace VoidCat.Services.Files;
/// <inheritdoc cref="IFileStore"/>
public class LocalDiskFileStore : StreamFileStore, IFileStore
{
private const string FilesDir = "files-v1";
private readonly VoidSettings _settings;
private readonly CompressContent _stripMetadata;
@ -17,12 +16,6 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
{
_settings = settings;
_stripMetadata = stripMetadata;
var dir = Path.Combine(_settings.DataDirectory, FilesDir);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
}
/// <inheritdoc />
@ -41,10 +34,15 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
/// <inheritdoc />
public string Key => "local-disk";
/// <inheritdoc />
public ValueTask<bool> Exists(Guid id)
{
var path = MapPath(id);
return ValueTask.FromResult(File.Exists(path));
}
public async ValueTask<Database.File> Ingress(IngressPayload payload, CancellationToken cts)
{
var finalPath = MapPath(payload.Id);
var finalPath = MapCreatePath(payload.Id);
await using var fsTemp = new FileStream(finalPath,
payload.IsAppend ? FileMode.Append : FileMode.Create, FileAccess.ReadWrite);
@ -97,7 +95,6 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
return vf;
}
/// <inheritdoc />
public ValueTask DeleteFile(Guid id)
{
var fp = MapPath(id);
@ -109,7 +106,6 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
return ValueTask.CompletedTask;
}
/// <inheritdoc />
public ValueTask<Stream> Open(EgressRequest request, CancellationToken cts)
{
var path = MapPath(request.Id);
@ -118,6 +114,18 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
return ValueTask.FromResult<Stream>(new FileStream(path, FileMode.Open, FileAccess.Read));
}
private string MapCreatePath(Guid id)
{
var path = MapPath(id);
var dir = Path.GetDirectoryName(path);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir!);
}
return path;
}
private string MapPath(Guid id) =>
Path.Join(_settings.DataDirectory, FilesDir, id.ToString());
Path.Join(_settings.DataDirectory, "files-v2", id.ToString()[..2], id.ToString()[2..4], id.ToString());
}

View File

@ -66,12 +66,8 @@ public class PostgresFileMetadataStore : IFileMetadataStore
/// <inheritdoc />
public async ValueTask<PagedResult<Database.File>> ListFiles(PagedRequest request)
{
var count = await _db.Files.CountAsync();
async IAsyncEnumerable<Database.File> Enumerate()
IQueryable<Database.File> MakeQuery(VoidContext db)
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<VoidContext>();
var q = db.Files.AsNoTracking().AsQueryable();
switch (request.SortBy, request.SortOrder)
{
@ -101,7 +97,15 @@ public class PostgresFileMetadataStore : IFileMetadataStore
break;
}
await foreach (var r in q.Skip(request.Page * request.PageSize).Take(request.PageSize).AsAsyncEnumerable())
return q.Skip(request.Page * request.PageSize).Take(request.PageSize);
}
async IAsyncEnumerable<Database.File> Enumerate()
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<VoidContext>();
await foreach (var r in MakeQuery(db).AsAsyncEnumerable())
{
yield return r;
}
@ -109,7 +113,7 @@ public class PostgresFileMetadataStore : IFileMetadataStore
return new()
{
TotalResults = count,
TotalResults = await MakeQuery(_db).CountAsync(),
PageSize = request.PageSize,
Page = request.Page,
Results = Enumerate()

View File

@ -24,10 +24,26 @@ public class S3FileStore : StreamFileStore, IFileStore
_client = _config.CreateClient();
}
/// <inheritdoc />
public string Key => _config.Name;
/// <inheritdoc />
public async ValueTask<bool> Exists(Guid id)
{
try
{
await _client.GetObjectMetadataAsync(new GetObjectMetadataRequest()
{
BucketName = _config.BucketName,
Key = id.ToString()
});
return true;
}
catch
{
return false;
}
}
public async ValueTask<Database.File> Ingress(IngressPayload payload, CancellationToken cts)
{
if (payload.IsMultipart) return await IngressMultipart(payload, cts);
@ -206,7 +222,7 @@ public class S3FileStore : StreamFileStore, IFileStore
InputStream = fsTmp,
DisablePayloadSigning = _config.DisablePayloadSigning
};
var bodyResponse = await _client.UploadPartAsync(mBody, cts);
if (bodyResponse.HttpStatusCode != HttpStatusCode.OK)
{
@ -241,7 +257,7 @@ public class S3FileStore : StreamFileStore, IFileStore
throw new Exception("Upload failed");
}
}
return HandleCompletedUpload(payload, segmentLength);
}

View File

@ -0,0 +1,80 @@
using VoidCat.Model;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Files;
namespace VoidCat.Services.Migrations;
public class CleanupLocalDiskStore : IMigration
{
private readonly VoidSettings _settings;
private readonly IFileMetadataStore _metadataStore;
private readonly IFileStore _fileStore;
private readonly ILogger<CleanupLocalDiskStore> _logger;
public CleanupLocalDiskStore(VoidSettings settings, IFileMetadataStore store, ILogger<CleanupLocalDiskStore> logger,
IFileStore fileStore)
{
_settings = settings;
_metadataStore = store;
_logger = logger;
_fileStore = fileStore;
}
public int Order => 3;
public async ValueTask<IMigration.MigrationResult> Migrate(string[] args)
{
if (_fileStore is not LocalDiskFileStore)
{
return IMigration.MigrationResult.Skipped;
}
await CleanupDisk();
await CleanupMetadata();
return IMigration.MigrationResult.Completed;
}
private async Task CleanupDisk()
{
var baseDir = Path.Join(_settings.DataDirectory, "files-v2");
foreach (var path in Directory.EnumerateFiles(baseDir, "*.*", SearchOption.AllDirectories))
{
if (!Guid.TryParse(Path.GetFileNameWithoutExtension(path), out var id))
{
continue;
}
var meta = await _metadataStore.Get(id);
if (meta == default)
{
_logger.LogInformation("Deleting unmapped file {Path}", path);
File.Delete(path);
}
}
}
private async Task CleanupMetadata()
{
var page = 0;
while (true)
{
var deleting = new List<Guid>();
var fileList = await _metadataStore.ListFiles(new(page++, 1000));
if (fileList.TotalResults == 0) break;
await foreach (var md in fileList.Results)
{
if (!await _fileStore.Exists(md.Id))
{
deleting.Add(md.Id);
}
}
foreach (var toDelete in deleting)
{
_logger.LogInformation("Deleting metadata with missing file {Id}", toDelete);
await _metadataStore.Delete(toDelete);
}
}
}
}

View File

@ -0,0 +1,42 @@
using VoidCat.Model;
namespace VoidCat.Services.Migrations;
public class FileStoreV2 : IMigration
{
private readonly VoidSettings _settings;
public FileStoreV2(VoidSettings settings)
{
_settings = settings;
}
public int Order => 2;
public ValueTask<IMigration.MigrationResult> Migrate(string[] args)
{
var baseDir = Path.Join(_settings.DataDirectory, "files-v1");
foreach (var path in Directory.EnumerateFiles(baseDir))
{
if (!Guid.TryParse(Path.GetFileNameWithoutExtension(path), out var id))
{
continue;
}
var dest = MapPathV2(id);
var destDir = Path.GetDirectoryName(dest)!;
if (!Directory.Exists(destDir))
{
Directory.CreateDirectory(destDir);
}
File.Move(MapPathV1(id), dest);
}
return ValueTask.FromResult(IMigration.MigrationResult.Completed);
}
private string MapPathV1(Guid id) =>
Path.Join(_settings.DataDirectory, "files-v1", id.ToString());
private string MapPathV2(Guid id) =>
Path.Join(_settings.DataDirectory, "files-v2", id.ToString()[..2], id.ToString()[2..4], id.ToString());
}

View File

@ -171,6 +171,9 @@ public static class VoidStartup
services.AddTransient<IMigration, EFMigrationSetup>();
services.AddTransient<IMigration, EFMigration>();
}
services.AddTransient<IMigration, FileStoreV2>();
services.AddTransient<IMigration, CleanupLocalDiskStore>();
}
public static JsonSerializerSettings ConfigJsonSettings(JsonSerializerSettings s)

View File

@ -2,7 +2,8 @@
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
},
"AllowedHosts": "*",
@ -10,7 +11,8 @@
"SiteUrl": "https://localhost:7195",
"DataDirectory": "./data",
"CorsOrigins": [
"http://localhost:3000"
"http://localhost:3000",
"http://localhost:8080"
]
}
}