// Copyright © 2020 The CefSharp Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
//
using System;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using CefSharp.Callback;
namespace CefSharp.Example
{
///
/// WORK IN PROGRESS - at this point this is a rough working demo,
/// it's not production tested.
///
internal class AcceptRangeResourceHandler : IResourceHandler
{
///
/// We reuse a temp buffer where possible for copying the data from the stream
/// into the output stream
///
private byte[] tempBuffer;
private readonly FileInfo fileInfo;
private string rangeHeader;
private bool isRangeRequest;
private string mimeType;
private Stream stream;
public AcceptRangeResourceHandler(string fileName)
{
fileInfo = new FileInfo(fileName);
mimeType = Cef.GetMimeType(fileInfo.Extension);
}
bool IResourceHandler.Open(IRequest request, out bool handleRequest, ICallback callback)
{
handleRequest = true;
rangeHeader = request.GetHeaderByName("Range");
isRangeRequest = !string.IsNullOrEmpty(rangeHeader);
if (isRangeRequest)
{
//Range header present, we'll now open the stream for read
//We don't need the stream open for the initial request as
//we just need it's length which we get from the FileInfo
stream = fileInfo.OpenRead();
}
return true;
}
void IResourceHandler.GetResponseHeaders(IResponse response, out long responseLength, out string redirectUrl)
{
responseLength = -1;
redirectUrl = null;
if (isRangeRequest)
{
var headers = new NameValueCollection();
headers.Add("Date", DateTime.Now.ToString("R"));
headers.Add("Last-Modified", fileInfo.LastWriteTime.ToString("R"));
headers.Add("Content-Type", mimeType);
string contentRange;
int contentLength;
if (TryGetRangeHeader(out contentRange, out contentLength))
{
headers.Add("Content-Length", contentLength.ToString());
headers.Add("Content-Range", contentRange);
responseLength = contentLength;
response.MimeType = mimeType;
response.Headers = headers;
response.StatusCode = (int)HttpStatusCode.PartialContent;
response.StatusText = "Partial Content";
}
else
{
responseLength = -1;
response.MimeType = mimeType;
response.Headers = headers;
response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
response.StatusText = "Requested Range Not Satisfiable";
}
}
else
{
//First request we only set headers
//Response headers
var headers = new NameValueCollection
{
{ "Accept-Ranges", "bytes" },
{ "Date", DateTime.Now.ToString("R") },
{ "Last-Modified", fileInfo.LastWriteTime.ToString("R") },
{ "Content-Length", fileInfo.Length.ToString() },
{ "Content-Type", mimeType }
};
responseLength = fileInfo.Length;
response.StatusCode = (int)HttpStatusCode.OK;
response.StatusText = "OK";
response.Headers = headers;
response.MimeType = mimeType;
}
}
private bool TryGetRangeHeader(out string contentRange, out int contentLength)
{
contentRange = "";
contentLength = 0;
//TODO Error handling
var range = rangeHeader.Substring(6).Split('-');
var rangeStart = string.IsNullOrEmpty(range[0]) ? 0 : int.Parse(range[0]);
var rangeEnd = string.IsNullOrEmpty(range[1]) ? 0 : int.Parse(range[1]);
var totalBytes = (int)stream.Length;
if (totalBytes == 0)
{
return false;
}
if (rangeEnd == 0)
{
rangeEnd = totalBytes - 1;
}
if (rangeStart > rangeEnd)
{
return false;
}
if (rangeStart != stream.Position)
{
stream.Seek(rangeStart, SeekOrigin.Begin);
}
contentRange = "bytes " + rangeStart + "-" + rangeEnd + "/" + totalBytes;
contentLength = totalBytes - rangeStart;
return true;
}
bool IResourceHandler.Skip(long bytesToSkip, out long bytesSkipped, IResourceSkipCallback callback)
{
//We don't need the callback, as it's an unmanaged resource we should dispose it (could wrap it in a using statement).
callback.Dispose();
//No Stream or Stream cannot seek then we indicate failure
if (stream == null || !stream.CanSeek)
{
//Indicate failure
bytesSkipped = -2;
return false;
}
if (stream.Position == (stream.Length - 1))
{
bytesSkipped = 0;
return true;
}
var oldPosition = stream.Position;
var position = stream.Seek(bytesToSkip, SeekOrigin.Current);
bytesSkipped = position - oldPosition;
return true;
}
bool IResourceHandler.Read(Stream dataOut, out int bytesRead, IResourceReadCallback callback)
{
bytesRead = 0;
//We don't need the callback, as it's an unmanaged resource we should dispose it (could wrap it in a using statement).
callback.Dispose();
if (stream == null)
{
return false;
}
//Data out represents an underlying unmanaged buffer (typically 64kb in size).
//We reuse a temp buffer where possible
if (tempBuffer == null || tempBuffer.Length < dataOut.Length)
{
tempBuffer = new byte[dataOut.Length];
}
bytesRead = stream.Read(tempBuffer, 0, tempBuffer.Length);
// To indicate response completion set bytesRead to 0 and return false
if (bytesRead == 0)
{
return false;
}
//We need to use bytesRead instead of tempbuffer.Length otherwise
//garbage from the previous copy would be written to dataOut
dataOut.Write(tempBuffer, 0, bytesRead);
return bytesRead > 0;
}
void IResourceHandler.Cancel()
{
}
void IDisposable.Dispose()
{
stream = null;
tempBuffer = null;
}
//NOT USED
bool IResourceHandler.ReadResponse(Stream dataOut, out int bytesRead, ICallback callback)
{
throw new NotImplementedException();
}
//NOT USED
bool IResourceHandler.ProcessRequest(IRequest request, ICallback callback)
{
throw new NotImplementedException();
}
}
}