forked from Kieran/void.cat
Add range requests
This commit is contained in:
parent
3321e93478
commit
ff9099d33f
@ -1,3 +1,4 @@
|
|||||||
|
using System.Net;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
using VoidCat.Services;
|
using VoidCat.Services;
|
||||||
@ -8,10 +9,20 @@ namespace VoidCat.Controllers;
|
|||||||
public class DownloadController : Controller
|
public class DownloadController : Controller
|
||||||
{
|
{
|
||||||
private readonly IFileStore _storage;
|
private readonly IFileStore _storage;
|
||||||
|
private readonly ILogger<DownloadController> _logger;
|
||||||
|
|
||||||
public DownloadController(IFileStore storage)
|
public DownloadController(IFileStore storage, ILogger<DownloadController> logger)
|
||||||
{
|
{
|
||||||
_storage = storage;
|
_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)]
|
[ResponseCache(Location = ResponseCacheLocation.Any, Duration = 86400)]
|
||||||
@ -20,16 +31,78 @@ public class DownloadController : Controller
|
|||||||
public async Task DownloadFile([FromRoute] string id)
|
public async Task DownloadFile([FromRoute] string id)
|
||||||
{
|
{
|
||||||
var gid = id.FromBase58Guid();
|
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)
|
if (meta == null)
|
||||||
{
|
{
|
||||||
Response.StatusCode = 404;
|
Response.StatusCode = 404;
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Response.Headers.XFrameOptions = "SAMEORIGIN";
|
Response.Headers.XFrameOptions = "SAMEORIGIN";
|
||||||
Response.Headers.ContentDisposition = $"inline; filename=\"{meta?.Metadata?.Name}\"";
|
Response.Headers.ContentDisposition = $"inline; filename=\"{meta?.Metadata?.Name}\"";
|
||||||
Response.ContentType = meta?.Metadata?.MimeType ?? "application/octet-stream";
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,10 +8,33 @@ namespace VoidCat.Services
|
|||||||
|
|
||||||
Task<InternalVoidFile> Ingress(Stream inStream, VoidFileMeta meta, CancellationToken cts);
|
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);
|
Task UpdateInfo(VoidFile patch, Guid editSecret);
|
||||||
|
|
||||||
IAsyncEnumerable<VoidFile> ListFiles();
|
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() ?? "*"}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,18 +28,19 @@ public class LocalDiskFileIngressFactory : IFileStore
|
|||||||
return await _metadataStore.Get(id);
|
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);
|
var path = MapPath(request.Id);
|
||||||
if (!File.Exists(path)) throw new VoidFileNotFoundException(id);
|
if (!File.Exists(path)) throw new VoidFileNotFoundException(request.Id);
|
||||||
|
|
||||||
await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
|
await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||||
using var buffer = MemoryPool<byte>.Shared.Rent();
|
if (request.Ranges.Any())
|
||||||
var readLength = 0;
|
|
||||||
while ((readLength = await fs.ReadAsync(buffer.Memory, cts)) > 0)
|
|
||||||
{
|
{
|
||||||
await outStream.WriteAsync(buffer.Memory[..readLength], cts);
|
await EgressRanges(request.Id, request.Ranges, fs, outStream, cts);
|
||||||
await _stats.TrackEgress(id, (ulong)readLength);
|
}
|
||||||
|
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) =>
|
private string MapPath(Guid id) =>
|
||||||
Path.Join(_settings.DataDirectory, id.ToString());
|
Path.Join(_settings.DataDirectory, id.ToString());
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user