Refactor NetworkFileStream replace obsolete WebRequest

This commit is contained in:
Michael Bucari-Tovo 2022-12-17 11:28:17 -07:00
parent 36efbcb812
commit 96c45c33e5
2 changed files with 51 additions and 190 deletions

View File

@ -156,12 +156,7 @@ namespace AaxDecrypter
private NetworkFileStreamPersister NewNetworkFilePersister() private NetworkFileStreamPersister NewNetworkFilePersister()
{ {
var headers = new System.Net.WebHeaderCollection var networkFileStream = new NetworkFileStream(TempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, new() { { "User-Agent", DownloadOptions.UserAgent } });
{
{ "User-Agent", DownloadOptions.UserAgent }
};
var networkFileStream = new NetworkFileStream(TempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, headers);
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState); return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
} }
} }

View File

@ -1,35 +1,16 @@
using Dinah.Core; using Dinah.Core;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
namespace AaxDecrypter namespace AaxDecrypter
{ {
/// <summary>
/// A <see cref="CookieContainer"/> for a single Uri.
/// </summary>
public class SingleUriCookieContainer : CookieContainer
{
private Uri baseAddress;
public Uri Uri
{
get => baseAddress;
set
{
baseAddress = new UriBuilder(value.Scheme, value.Host).Uri;
}
}
public CookieCollection GetCookies()
{
return GetCookies(Uri);
}
}
/// <summary> /// <summary>
/// A resumable, simultaneous file downloader and reader. /// A resumable, simultaneous file downloader and reader.
/// </summary> /// </summary>
@ -51,17 +32,11 @@ namespace AaxDecrypter
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public Uri Uri { get; private set; } public Uri Uri { get; private set; }
/// <summary>
/// All cookies set by caller or by the remote server.
/// </summary>
[JsonProperty(Required = Required.Always)]
public SingleUriCookieContainer CookieContainer { get; }
/// <summary> /// <summary>
/// Http headers to be sent to the server with the request. /// Http headers to be sent to the server with the request.
/// </summary> /// </summary>
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public WebHeaderCollection RequestHeaders { get; private set; } public Dictionary<string, string> RequestHeaders { get; private set; }
/// <summary> /// <summary>
/// The position in <see cref="SaveFilePath"/> that has been written and flushed to disk. /// The position in <see cref="SaveFilePath"/> that has been written and flushed to disk.
@ -75,17 +50,16 @@ namespace AaxDecrypter
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public long ContentLength { get; private set; } public long ContentLength { get; private set; }
[JsonIgnore]
public bool IsCancelled { get; private set; }
#endregion #endregion
#region Private Properties #region Private Properties
private HttpWebRequest HttpRequest { get; set; }
private FileStream _writeFile { get; } private FileStream _writeFile { get; }
private FileStream _readFile { get; } private FileStream _readFile { get; }
private Stream _networkStream { get; set; } private EventWaitHandle _downloadedPiece { get; set; }
private bool hasBegunDownloading { get; set; } private Task _backgroundDownloadTask { get; set; }
public bool IsCancelled { get; private set; }
private EventWaitHandle downloadEnded { get; set; }
private EventWaitHandle downloadedPiece { get; set; }
#endregion #endregion
@ -110,7 +84,7 @@ namespace AaxDecrypter
/// <param name="writePosition">The position in <paramref name="uri"/> to begin downloading.</param> /// <param name="writePosition">The position in <paramref name="uri"/> to begin downloading.</param>
/// <param name="requestHeaders">Http headers to be sent to the server with the <see cref="HttpWebRequest"/>.</param> /// <param name="requestHeaders">Http headers to be sent to the server with the <see cref="HttpWebRequest"/>.</param>
/// <param name="cookies">A <see cref="SingleUriCookieContainer"/> with cookies to send with the <see cref="HttpWebRequest"/>. It will also be populated with any cookies set by the server. </param> /// <param name="cookies">A <see cref="SingleUriCookieContainer"/> with cookies to send with the <see cref="HttpWebRequest"/>. It will also be populated with any cookies set by the server. </param>
public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, WebHeaderCollection requestHeaders = null, SingleUriCookieContainer cookies = null) public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, Dictionary<string, string> requestHeaders = null)
{ {
ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath)); ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri)); ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri));
@ -122,8 +96,7 @@ namespace AaxDecrypter
SaveFilePath = saveFilePath; SaveFilePath = saveFilePath;
Uri = uri; Uri = uri;
WritePosition = writePosition; WritePosition = writePosition;
RequestHeaders = requestHeaders ?? new WebHeaderCollection(); RequestHeaders = requestHeaders ?? new();
CookieContainer = cookies ?? new SingleUriCookieContainer { Uri = uri };
_writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite) _writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
{ {
@ -144,7 +117,7 @@ namespace AaxDecrypter
/// </summary> /// </summary>
private void Update() private void Update()
{ {
RequestHeaders = HttpRequest.Headers; RequestHeaders["Range"] = $"bytes={WritePosition}-";
try try
{ {
Updated?.Invoke(this, EventArgs.Empty); Updated?.Invoke(this, EventArgs.Empty);
@ -165,37 +138,31 @@ namespace AaxDecrypter
if (uriToSameFile.Host != Uri.Host) if (uriToSameFile.Host != Uri.Host)
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}"); throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
if (hasBegunDownloading) if (_backgroundDownloadTask is not null)
throw new InvalidOperationException("Cannot change Uri after download has started."); throw new InvalidOperationException("Cannot change Uri after download has started.");
Uri = uriToSameFile; Uri = uriToSameFile;
HttpRequest = WebRequest.CreateHttp(Uri); RequestHeaders["Range"] = $"bytes={WritePosition}-";
HttpRequest.CookieContainer = CookieContainer;
HttpRequest.Headers = RequestHeaders;
//If NetworkFileStream is resuming, Header will already contain a range.
HttpRequest.Headers.Remove("Range");
HttpRequest.AddRange(WritePosition);
} }
/// <summary> /// <summary>
/// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread. /// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread.
/// </summary> /// </summary>
private void BeginDownloading() private Task BeginDownloading()
{ {
downloadEnded = new EventWaitHandle(false, EventResetMode.ManualReset);
if (ContentLength != 0 && WritePosition == ContentLength) if (ContentLength != 0 && WritePosition == ContentLength)
{ return Task.CompletedTask;
hasBegunDownloading = true;
downloadEnded.Set();
return;
}
if (ContentLength != 0 && WritePosition > ContentLength) if (ContentLength != 0 && WritePosition > ContentLength)
throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10})."); throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10}).");
var response = HttpRequest.GetResponse() as HttpWebResponse;
var request = new HttpRequestMessage(HttpMethod.Get, Uri);
foreach (var header in RequestHeaders)
request.Headers.Add(header.Key, header.Value);
var response = new HttpClient().Send(request, HttpCompletionOption.ResponseHeadersRead);
if (response.StatusCode != HttpStatusCode.PartialContent) if (response.StatusCode != HttpStatusCode.PartialContent)
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}."); throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
@ -203,24 +170,19 @@ namespace AaxDecrypter
//Content length is the length of the range request, and it is only equal //Content length is the length of the range request, and it is only equal
//to the complete file length if requesting Range: bytes=0- //to the complete file length if requesting Range: bytes=0-
if (WritePosition == 0) if (WritePosition == 0)
ContentLength = response.ContentLength; ContentLength = response.Content.Headers.ContentLength.GetValueOrDefault();
_networkStream = response.GetResponseStream(); var networkStream = response.Content.ReadAsStream();
downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset); _downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Download the file in the background. //Download the file in the background.
new Thread(() => DownloadFile()) return Task.Run(() => DownloadFile(networkStream));
{ IsBackground = true }
.Start();
hasBegunDownloading = true;
return;
} }
/// <summary> /// <summary>
/// Download <see cref="Uri"/> to <see cref="SaveFilePath"/>. /// Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.
/// </summary> /// </summary>
private void DownloadFile() private void DownloadFile(Stream networkStream)
{ {
var downloadPosition = WritePosition; var downloadPosition = WritePosition;
var nextFlush = downloadPosition + DATA_FLUSH_SZ; var nextFlush = downloadPosition + DATA_FLUSH_SZ;
@ -231,7 +193,7 @@ namespace AaxDecrypter
int bytesRead; int bytesRead;
do do
{ {
bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ); bytesRead = networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
_writeFile.Write(buff, 0, bytesRead); _writeFile.Write(buff, 0, bytesRead);
downloadPosition += bytesRead; downloadPosition += bytesRead;
@ -242,15 +204,12 @@ namespace AaxDecrypter
WritePosition = downloadPosition; WritePosition = downloadPosition;
Update(); Update();
nextFlush = downloadPosition + DATA_FLUSH_SZ; nextFlush = downloadPosition + DATA_FLUSH_SZ;
downloadedPiece.Set(); _downloadedPiece.Set();
} }
} while (downloadPosition < ContentLength && !IsCancelled && bytesRead > 0); } while (downloadPosition < ContentLength && !IsCancelled && bytesRead > 0);
_writeFile.Close();
_networkStream.Close();
WritePosition = downloadPosition; WritePosition = downloadPosition;
Update();
if (!IsCancelled && WritePosition < ContentLength) if (!IsCancelled && WritePosition < ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10})."); throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
@ -264,8 +223,10 @@ namespace AaxDecrypter
} }
finally finally
{ {
downloadedPiece.Set(); networkStream.Close();
downloadEnded.Set(); _writeFile.Close();
_downloadedPiece.Set();
Update();
} }
} }
@ -274,96 +235,7 @@ namespace AaxDecrypter
#region Json Connverters #region Json Connverters
public static JsonSerializerSettings GetJsonSerializerSettings() public static JsonSerializerSettings GetJsonSerializerSettings()
{ => new JsonSerializerSettings();
var settings = new JsonSerializerSettings();
settings.Converters.Add(new CookieContainerConverter());
settings.Converters.Add(new WebHeaderCollectionConverter());
return settings;
}
internal class CookieContainerConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(SingleUriCookieContainer);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var result = new SingleUriCookieContainer()
{
Uri = new Uri(jObj["Uri"].Value<string>()),
Capacity = jObj["Capacity"].Value<int>(),
MaxCookieSize = jObj["MaxCookieSize"].Value<int>(),
PerDomainCapacity = jObj["PerDomainCapacity"].Value<int>()
};
var cookieList = jObj["Cookies"].ToList();
foreach (var cookie in cookieList)
{
result.Add(
new Cookie
{
Comment = cookie["Comment"].Value<string>(),
HttpOnly = cookie["HttpOnly"].Value<bool>(),
Discard = cookie["Discard"].Value<bool>(),
Domain = cookie["Domain"].Value<string>(),
Expired = cookie["Expired"].Value<bool>(),
Expires = cookie["Expires"].Value<DateTime>(),
Name = cookie["Name"].Value<string>(),
Path = cookie["Path"].Value<string>(),
Port = cookie["Port"].Value<string>(),
Secure = cookie["Secure"].Value<bool>(),
Value = cookie["Value"].Value<string>(),
Version = cookie["Version"].Value<int>(),
});
}
return result;
}
public override bool CanWrite => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var cookies = value as SingleUriCookieContainer;
var obj = (JObject)JToken.FromObject(value);
var container = cookies.GetCookies();
var propertyNames = container.Select(c => JToken.FromObject(c));
obj.AddFirst(new JProperty("Cookies", new JArray(propertyNames)));
obj.WriteTo(writer);
}
}
internal class WebHeaderCollectionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(WebHeaderCollection);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var result = new WebHeaderCollection();
foreach (var kvp in jObj)
result.Add(kvp.Key, kvp.Value.Value<string>());
return result;
}
public override bool CanWrite => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var jObj = new JObject();
var type = value.GetType();
var headers = value as WebHeaderCollection;
var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k]));
jObj.Add(jHeaders);
jObj.WriteTo(writer);
}
}
#endregion #endregion
@ -383,8 +255,7 @@ namespace AaxDecrypter
{ {
get get
{ {
if (!hasBegunDownloading) _backgroundDownloadTask ??= BeginDownloading();
BeginDownloading();
return ContentLength; return ContentLength;
} }
} }
@ -401,14 +272,13 @@ namespace AaxDecrypter
[JsonIgnore] [JsonIgnore]
public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; } public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; }
public override void Flush() => throw new NotImplementedException(); public override void Flush() => throw new InvalidOperationException();
public override void SetLength(long value) => throw new NotImplementedException(); public override void SetLength(long value) => throw new InvalidOperationException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); public override void Write(byte[] buffer, int offset, int count) => throw new InvalidOperationException();
public override int Read(byte[] buffer, int offset, int count) public override int Read(byte[] buffer, int offset, int count)
{ {
if (!hasBegunDownloading) _backgroundDownloadTask ??= BeginDownloading();
BeginDownloading();
var toRead = Math.Min(count, Length - Position); var toRead = Math.Min(count, Length - Position);
WaitToPosition(Position + toRead); WaitToPosition(Position + toRead);
@ -435,31 +305,27 @@ namespace AaxDecrypter
private void WaitToPosition(long requiredPosition) private void WaitToPosition(long requiredPosition)
{ {
while (WritePosition < requiredPosition while (WritePosition < requiredPosition
&& hasBegunDownloading && _backgroundDownloadTask?.IsCompleted is false
&& !IsCancelled && !IsCancelled)
&& !downloadEnded.WaitOne(0))
{ {
downloadedPiece.WaitOne(100); _downloadedPiece.WaitOne(100);
} }
} }
public override void Close() public override void Close()
{ {
IsCancelled = true; IsCancelled = true;
_backgroundDownloadTask?.Wait();
while (downloadEnded is not null && !downloadEnded.WaitOne(100)) ;
_readFile.Close(); _readFile.Close();
_writeFile.Close(); _writeFile.Close();
_networkStream?.Close();
Update(); Update();
} }
#endregion #endregion
~NetworkFileStream() ~NetworkFileStream()
{ {
downloadEnded?.Close(); _downloadedPiece?.Close();
downloadedPiece?.Close();
} }
} }
} }