Skip to content

Instantly share code, notes, and snippets.

@savitskiyan
Last active November 11, 2015 07:03
Show Gist options
  • Save savitskiyan/7bffe67a64c238895172 to your computer and use it in GitHub Desktop.
Save savitskiyan/7bffe67a64c238895172 to your computer and use it in GitHub Desktop.
ASP.NET MVC 6 WebAPI support partial download file
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
namespace Server.Controllers
{
public sealed class FileActionResult : IActionResult
{
public const int BufferSize = ushort.MaxValue;
private readonly string _filename;
private readonly string _contentType;
public FileActionResult(string filename, string contentType)
{
if (filename == null)
throw new ArgumentNullException(nameof(filename), "Не задано имя файла");
if (contentType == null)
throw new ArgumentNullException(nameof(contentType), "Не задан тип контента");
_filename = filename;
_contentType = contentType;
}
private static async Task CreatePartialContent(Stream input, Stream output, long start, long end)
{
var remainingBytes = end - start + 1;
long position;
var buffer = new byte[BufferSize];
input.Position = start;
do
{
try
{
var count = remainingBytes > BufferSize
? input.Read(buffer, 0, BufferSize)
: input.Read(buffer, 0, (int) remainingBytes);
await output.WriteAsync(buffer, 0, count);
}
catch
{
break;
}
position = input.Position;
remainingBytes = end - position + 1;
} while (position <= end);
}
private static bool TryReadRangeItem(RangeItemHeaderValue range, long contentLength, out long start, out long end)
{
if (range.From != null)
{
start = range.From.Value;
end = range.To ?? contentLength - 1;
}
else
{
end = contentLength - 1;
start = contentLength - range.To ?? 0;
}
return (start < contentLength && end < contentLength);
}
public async Task ExecuteResultAsync(ActionContext context)
{
var fileInfo = new FileInfo(_filename);
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
response.ContentType = _contentType;
if (!fileInfo.Exists)
{
response.StatusCode = (int) HttpStatusCode.NotFound;
}
else
{
var header = request.Headers["Range"].FirstOrDefault();
RangeHeaderValue rangeHeader = null;
if (header != null)
rangeHeader = RangeHeaderValue.Parse(header);
long start, end;
response.Headers["Accept-Ranges"] = "bytes";
// The request will be treated as normal request if there is no Range header.
if (rangeHeader == null || !rangeHeader.Ranges.Any())
{
response.ContentLength = fileInfo.Length;
response.StatusCode = (int)HttpStatusCode.OK;
try
{
using (var input = GetFileStream(fileInfo.FullName))
await input.CopyToAsync(response.Body, 4096);
}
catch
{
}
}
else if (rangeHeader.Unit != "bytes" || rangeHeader.Ranges.Count > 1 || !TryReadRangeItem(rangeHeader.Ranges.First(), fileInfo.Length, out start, out end))
{
// 1. If the unit is not 'bytes'.
// 2. If there are multiple ranges in header value.
// 3. If start or end position is greater than file length.
response.Headers["Content-Range"] = new ContentRangeHeaderValue(0, fileInfo.Length).ToString();
response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
}
else
{
// We are now ready to produce partial content.
response.ContentLength = end - start + 1;
response.Headers["Content-Range"] = new ContentRangeHeaderValue(start, end, fileInfo.Length).ToString();
response.StatusCode = (int)HttpStatusCode.PartialContent;
try
{
using (var input = GetFileStream(fileInfo.FullName))
await CreatePartialContent(input, response.Body, start, end);
}
catch
{
}
}
}
}
private static Stream GetFileStream(string path)
{
return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment