Add range requests

This commit is contained in:
Kieran 2022-02-08 23:52:01 +00:00
parent 3321e93478
commit ff9099d33f
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
3 changed files with 154 additions and 21 deletions

View File

@ -1,3 +1,4 @@
using System.Net;
using Microsoft.AspNetCore.Mvc;
using VoidCat.Model;
using VoidCat.Services;
@ -8,10 +9,20 @@ namespace VoidCat.Controllers;
public class DownloadController : Controller
{
private readonly IFileStore _storage;
private readonly ILogger<DownloadController> _logger;
public DownloadController(IFileStore storage)
public DownloadController(IFileStore storage, ILogger<DownloadController> logger)
{
_storage = storage;
_logger = logger;
}
[HttpOptions]
[Route("{id}")]
public Task DownloadFileOptions([FromRoute] string id)
{
var gid = id.FromBase58Guid();
return SetupDownload(gid);
}
[ResponseCache(Location = ResponseCacheLocation.Any, Duration = 86400)]
@ -20,16 +31,78 @@ public class DownloadController : Controller
public async Task DownloadFile([FromRoute] string id)
{
var gid = id.FromBase58Guid();
var meta = await _storage.Get(gid);
var meta = await SetupDownload(gid);
var egressReq = new EgressRequest(gid, GetRanges(Request, (long)meta!.Size));
if (egressReq.Ranges.Count() > 1)
{
_logger.LogWarning("Multi-range request not supported!");
// downgrade to full send
egressReq = egressReq with
{
Ranges = Enumerable.Empty<RangeRequest>()
};
}
else if(egressReq.Ranges.Count() == 1)
{
Response.StatusCode = (int)HttpStatusCode.PartialContent;
}
else
{
Response.Headers.AcceptRanges = "bytes";
}
foreach (var range in egressReq.Ranges)
{
Response.Headers.Add("content-range", range.ToContentRange());
Response.ContentLength = range.Size;
}
var cts = HttpContext.RequestAborted;
await Response.StartAsync(cts);
await _storage.Egress(egressReq, Response.Body, cts);
await Response.CompleteAsync();
}
private async Task<VoidFile?> SetupDownload(Guid id)
{
var meta = await _storage.Get(id);
if (meta == null)
{
Response.StatusCode = 404;
return;
return null;
}
Response.Headers.XFrameOptions = "SAMEORIGIN";
Response.Headers.ContentDisposition = $"inline; filename=\"{meta?.Metadata?.Name}\"";
Response.ContentType = meta?.Metadata?.MimeType ?? "application/octet-stream";
await _storage.Egress(gid, Response.Body, HttpContext.RequestAborted);
return meta;
}
private IEnumerable<RangeRequest> GetRanges(HttpRequest request, long totalSize)
{
foreach (var rangeHeader in request.Headers.Range)
{
if (string.IsNullOrEmpty(rangeHeader))
{
continue;
}
var ranges = rangeHeader.Replace("bytes=", string.Empty).Split(",");
foreach (var range in ranges)
{
var rangeValues = range.Split("-");
long? endByte = null, startByte = 0;
if (long.TryParse(rangeValues[1], out var endParsed))
endByte = endParsed;
if (long.TryParse(rangeValues[0], out var startParsed))
startByte = startParsed;
yield return new(totalSize, startByte, endByte);
}
}
}
}

View File

@ -8,10 +8,33 @@ namespace VoidCat.Services
Task<InternalVoidFile> Ingress(Stream inStream, VoidFileMeta meta, CancellationToken cts);
Task Egress(Guid id, Stream outStream, CancellationToken cts);
Task Egress(EgressRequest request, Stream outStream, CancellationToken cts);
Task UpdateInfo(VoidFile patch, Guid editSecret);
IAsyncEnumerable<VoidFile> ListFiles();
}
public record EgressRequest(Guid Id, IEnumerable<RangeRequest> Ranges)
{
}
public record RangeRequest(long? TotalSize, long? Start, long? End)
{
private const long DefaultBufferSize = 1024L * 512L;
public long? Size
=> Start.HasValue ?
(End ?? Math.Min(TotalSize!.Value, Start.Value + DefaultBufferSize)) - Start.Value : End;
public bool IsForFullFile
=> Start is 0 && !End.HasValue;
/// <summary>
/// Return Content-Range header content for this range
/// </summary>
/// <returns></returns>
public string ToContentRange()
=> $"bytes {Start}-{End ?? (Start + Size - 1L)}/{TotalSize?.ToString() ?? "*"}";
}
}

View File

@ -28,18 +28,19 @@ public class LocalDiskFileIngressFactory : IFileStore
return await _metadataStore.Get(id);
}
public async Task Egress(Guid id, Stream outStream, CancellationToken cts)
public async Task Egress(EgressRequest request, Stream outStream, CancellationToken cts)
{
var path = MapPath(id);
if (!File.Exists(path)) throw new VoidFileNotFoundException(id);
var path = MapPath(request.Id);
if (!File.Exists(path)) throw new VoidFileNotFoundException(request.Id);
await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
using var buffer = MemoryPool<byte>.Shared.Rent();
var readLength = 0;
while ((readLength = await fs.ReadAsync(buffer.Memory, cts)) > 0)
if (request.Ranges.Any())
{
await outStream.WriteAsync(buffer.Memory[..readLength], cts);
await _stats.TrackEgress(id, (ulong)readLength);
await EgressRanges(request.Id, request.Ranges, fs, outStream, cts);
}
else
{
await EgressFull(request.Id, fs, outStream, cts);
}
}
@ -92,6 +93,42 @@ public class LocalDiskFileIngressFactory : IFileStore
}
}
private async Task EgressFull(Guid id, FileStream fileStream, Stream outStream,
CancellationToken cts)
{
using var buffer = MemoryPool<byte>.Shared.Rent();
var readLength = 0;
while ((readLength = await fileStream.ReadAsync(buffer.Memory, cts)) > 0)
{
await outStream.WriteAsync(buffer.Memory[..readLength], cts);
await _stats.TrackEgress(id, (ulong)readLength);
await outStream.FlushAsync(cts);
}
}
private async Task EgressRanges(Guid id, IEnumerable<RangeRequest> ranges, FileStream fileStream, Stream outStream,
CancellationToken cts)
{
using var buffer = MemoryPool<byte>.Shared.Rent();
foreach (var range in ranges)
{
fileStream.Seek(range.Start ?? range.End ?? 0L,
range.Start.HasValue ? SeekOrigin.Begin : SeekOrigin.End);
var readLength = 0;
var dataRemaining = range.Size ?? 0L;
while ((readLength = await fileStream.ReadAsync(buffer.Memory, cts)) > 0
&& dataRemaining > 0)
{
var toWrite = Math.Min(readLength, dataRemaining);
await outStream.WriteAsync(buffer.Memory[..(int)toWrite], cts);
await _stats.TrackEgress(id, (ulong)toWrite);
dataRemaining -= toWrite;
await outStream.FlushAsync(cts);
}
}
}
private string MapPath(Guid id) =>
Path.Join(_settings.DataDirectory, id.ToString());
}