void.cat/VoidCat/Services/Files/LocalDiskFileStorage.cs

243 lines
8.5 KiB
C#
Raw Normal View History

2022-01-25 23:39:51 +00:00
using System.Buffers;
2022-02-10 22:22:34 +00:00
using System.Security.Cryptography;
2022-01-25 23:39:51 +00:00
using VoidCat.Model;
using VoidCat.Model.Exceptions;
2022-02-16 16:33:00 +00:00
using VoidCat.Services.Abstractions;
2022-01-25 23:39:51 +00:00
2022-02-22 14:20:31 +00:00
namespace VoidCat.Services.Files;
2022-01-25 23:39:51 +00:00
2022-02-16 16:33:00 +00:00
public class LocalDiskFileStore : IFileStore
2022-01-25 23:39:51 +00:00
{
2022-02-22 14:20:31 +00:00
private const int BufferSize = 1_048_576;
private readonly ILogger<LocalDiskFileStore> _logger;
2022-01-25 23:39:51 +00:00
private readonly VoidSettings _settings;
2022-02-16 23:19:31 +00:00
private readonly IAggregateStatsCollector _stats;
2022-02-08 18:20:59 +00:00
private readonly IFileMetadataStore _metadataStore;
private readonly IFileInfoManager _fileInfo;
2022-02-08 23:52:01 +00:00
2022-02-22 14:20:31 +00:00
public LocalDiskFileStore(ILogger<LocalDiskFileStore> logger, VoidSettings settings, IAggregateStatsCollector stats,
IFileMetadataStore metadataStore, IFileInfoManager fileInfo)
2022-01-25 23:39:51 +00:00
{
_settings = settings;
_stats = stats;
2022-02-08 18:20:59 +00:00
_metadataStore = metadataStore;
_fileInfo = fileInfo;
2022-02-22 14:20:31 +00:00
_logger = logger;
2022-01-25 23:39:51 +00:00
if (!Directory.Exists(_settings.DataDirectory))
{
Directory.CreateDirectory(_settings.DataDirectory);
}
}
2022-02-16 16:33:00 +00:00
public async ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts)
2022-01-25 23:39:51 +00:00
{
2022-02-08 23:52:01 +00:00
var path = MapPath(request.Id);
if (!File.Exists(path)) throw new VoidFileNotFoundException(request.Id);
2022-01-25 23:39:51 +00:00
await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
2022-02-08 23:52:01 +00:00
if (request.Ranges.Any())
2022-01-25 23:39:51 +00:00
{
2022-02-08 23:52:01 +00:00
await EgressRanges(request.Id, request.Ranges, fs, outStream, cts);
}
else
{
await EgressFull(request.Id, fs, outStream, cts);
2022-01-25 23:39:51 +00:00
}
}
2022-02-17 15:52:49 +00:00
public async ValueTask<PrivateVoidFile> Ingress(IngressPayload payload, CancellationToken cts)
2022-01-25 23:39:51 +00:00
{
2022-02-10 22:22:34 +00:00
var id = payload.Id ?? Guid.NewGuid();
2022-01-25 23:39:51 +00:00
var fPath = MapPath(id);
2022-02-27 13:54:25 +00:00
var vf = payload.Meta;
2022-02-10 22:22:34 +00:00
if (payload.IsAppend)
2022-01-25 23:39:51 +00:00
{
2022-02-10 22:22:34 +00:00
if (vf?.EditSecret != null && vf.EditSecret != payload.EditSecret)
{
throw new VoidNotAllowedException("Edit secret incorrect!");
}
2022-01-25 23:39:51 +00:00
}
2022-02-08 23:52:01 +00:00
2022-02-10 22:22:34 +00:00
// open file
2022-02-16 16:33:00 +00:00
await using var fsTemp = new FileStream(fPath,
2022-02-10 22:22:34 +00:00
payload.IsAppend ? FileMode.Append : FileMode.Create, FileAccess.Write);
var (total, hash) = await IngressInternal(id, payload.InStream, fsTemp, cts);
if (!hash.Equals(payload.Hash, StringComparison.InvariantCultureIgnoreCase))
{
throw new CryptographicException("Invalid file hash");
}
2022-02-16 16:33:00 +00:00
2022-02-10 22:22:34 +00:00
if (payload.IsAppend)
{
vf = vf! with
{
Size = vf.Size + total
};
}
else
2022-01-25 23:39:51 +00:00
{
2022-02-27 13:54:25 +00:00
vf = vf! with
2022-02-10 22:22:34 +00:00
{
Uploaded = DateTimeOffset.UtcNow,
EditSecret = Guid.NewGuid(),
Size = total
};
}
2022-02-17 15:52:49 +00:00
await _metadataStore.Set(id, vf);
return new()
{
Id = id,
Metadata = vf
};
2022-02-08 18:20:59 +00:00
}
2022-02-08 23:52:01 +00:00
2022-02-24 12:00:28 +00:00
public ValueTask<PagedResult<PublicVoidFile>> ListFiles(PagedRequest request)
2022-02-08 18:20:59 +00:00
{
var files = Directory.EnumerateFiles(_settings.DataDirectory)
.Where(a => !Path.HasExtension(a));
2022-02-24 12:00:28 +00:00
files = (request.SortBy, request.SortOrder) switch
2022-01-25 23:39:51 +00:00
{
(PagedSortBy.Id, PageSortOrder.Asc) => files.OrderBy(a =>
Guid.TryParse(Path.GetFileNameWithoutExtension(a), out var g) ? g : Guid.Empty),
(PagedSortBy.Id, PageSortOrder.Dsc) => files.OrderByDescending(a =>
Guid.TryParse(Path.GetFileNameWithoutExtension(a), out var g) ? g : Guid.Empty),
(PagedSortBy.Name, PageSortOrder.Asc) => files.OrderBy(Path.GetFileNameWithoutExtension),
(PagedSortBy.Name, PageSortOrder.Dsc) => files.OrderByDescending(Path.GetFileNameWithoutExtension),
(PagedSortBy.Size, PageSortOrder.Asc) => files.OrderBy(a => new FileInfo(a).Length),
(PagedSortBy.Size, PageSortOrder.Dsc) => files.OrderByDescending(a => new FileInfo(a).Length),
(PagedSortBy.Date, PageSortOrder.Asc) => files.OrderBy(File.GetCreationTimeUtc),
(PagedSortBy.Date, PageSortOrder.Dsc) => files.OrderByDescending(File.GetCreationTimeUtc),
_ => files
};
2022-01-25 23:39:51 +00:00
async IAsyncEnumerable<PublicVoidFile> EnumeratePage(IEnumerable<string> page)
{
foreach (var file in page)
2022-02-08 18:20:59 +00:00
{
if (!Guid.TryParse(Path.GetFileNameWithoutExtension(file), out var gid)) continue;
2022-02-24 12:00:28 +00:00
var loaded = await _fileInfo.Get(gid);
if (loaded != default)
{
yield return loaded;
}
2022-02-08 18:20:59 +00:00
}
}
2022-02-24 12:00:28 +00:00
return ValueTask.FromResult(new PagedResult<PublicVoidFile>()
{
Page = request.Page,
PageSize = request.PageSize,
TotalResults = files.Count(),
Results = EnumeratePage(files.Skip(request.PageSize * request.Page).Take(request.PageSize))
2022-02-24 12:00:28 +00:00
});
2022-01-25 23:39:51 +00:00
}
2022-02-22 14:20:31 +00:00
public async ValueTask DeleteFile(Guid id)
{
var fp = MapPath(id);
if (File.Exists(fp))
{
_logger.LogInformation("Deleting file: {Path}", fp);
File.Delete(fp);
}
await _metadataStore.Delete(id);
}
2022-02-10 22:22:34 +00:00
private async Task<(ulong, string)> IngressInternal(Guid id, Stream ingress, Stream fs, CancellationToken cts)
{
2022-02-16 23:19:31 +00:00
using var buffer = MemoryPool<byte>.Shared.Rent(BufferSize);
2022-02-10 22:22:34 +00:00
var total = 0UL;
2022-02-16 23:19:31 +00:00
int readLength = 0, offset = 0;
2022-02-10 22:22:34 +00:00
var sha = SHA256.Create();
2022-02-16 23:19:31 +00:00
while ((readLength = await ingress.ReadAsync(buffer.Memory[offset..], cts)) > 0 || offset != 0)
2022-02-10 22:22:34 +00:00
{
2022-02-16 23:19:31 +00:00
if (readLength != 0 && offset + readLength < buffer.Memory.Length)
{
// read until buffer full
offset += readLength;
continue;
}
var totalRead = readLength + offset;
var buf = buffer.Memory[..totalRead];
2022-02-10 22:22:34 +00:00
await fs.WriteAsync(buf, cts);
2022-02-24 12:00:28 +00:00
await _stats.TrackIngress(id, (ulong)buf.Length);
2022-02-10 22:22:34 +00:00
sha.TransformBlock(buf.ToArray(), 0, buf.Length, null, 0);
2022-02-24 12:00:28 +00:00
total += (ulong)buf.Length;
2022-02-16 23:19:31 +00:00
offset = 0;
2022-02-10 22:22:34 +00:00
}
sha.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
return (total, BitConverter.ToString(sha.Hash!).Replace("-", string.Empty));
}
2022-02-08 23:52:01 +00:00
private async Task EgressFull(Guid id, FileStream fileStream, Stream outStream,
CancellationToken cts)
{
2022-02-16 23:19:31 +00:00
using var buffer = MemoryPool<byte>.Shared.Rent(BufferSize);
int readLength = 0, offset = 0;
while ((readLength = await fileStream.ReadAsync(buffer.Memory[offset..], cts)) > 0 || offset != 0)
2022-02-08 23:52:01 +00:00
{
2022-02-16 23:19:31 +00:00
if (readLength != 0 && offset + readLength < buffer.Memory.Length)
{
// read until buffer full
offset += readLength;
continue;
}
var fullSize = readLength + offset;
await outStream.WriteAsync(buffer.Memory[..fullSize], cts);
2022-02-24 12:00:28 +00:00
await _stats.TrackEgress(id, (ulong)fullSize);
2022-02-08 23:52:01 +00:00
await outStream.FlushAsync(cts);
2022-02-16 23:19:31 +00:00
offset = 0;
2022-02-08 23:52:01 +00:00
}
}
private async Task EgressRanges(Guid id, IEnumerable<RangeRequest> ranges, FileStream fileStream, Stream outStream,
CancellationToken cts)
{
2022-02-16 23:19:31 +00:00
using var buffer = MemoryPool<byte>.Shared.Rent(BufferSize);
2022-02-08 23:52:01 +00:00
foreach (var range in ranges)
{
2022-02-08 23:56:35 +00:00
fileStream.Seek(range.Start ?? range.End ?? 0L,
2022-02-08 23:52:01 +00:00
range.Start.HasValue ? SeekOrigin.Begin : SeekOrigin.End);
2022-02-16 23:19:31 +00:00
int readLength = 0, offset = 0;
2022-02-08 23:52:01 +00:00
var dataRemaining = range.Size ?? 0L;
2022-02-16 23:19:31 +00:00
while ((readLength = await fileStream.ReadAsync(buffer.Memory[offset..], cts)) > 0 || offset != 0)
2022-02-08 23:52:01 +00:00
{
2022-02-16 23:19:31 +00:00
if (readLength != 0 && offset + readLength < buffer.Memory.Length)
{
// read until buffer full
offset += readLength;
continue;
}
var fullSize = readLength + offset;
var toWrite = Math.Min(fullSize, dataRemaining);
2022-02-24 12:00:28 +00:00
await outStream.WriteAsync(buffer.Memory[..(int)toWrite], cts);
await _stats.TrackEgress(id, (ulong)toWrite);
2022-02-08 23:52:01 +00:00
await outStream.FlushAsync(cts);
2022-02-16 23:19:31 +00:00
dataRemaining -= toWrite;
offset = 0;
if (dataRemaining == 0)
{
break;
}
2022-02-08 23:52:01 +00:00
}
}
}
2022-01-25 23:39:51 +00:00
private string MapPath(Guid id) =>
Path.Join(_settings.DataDirectory, id.ToString());
2022-02-24 12:00:28 +00:00
}