using Dinah.Core; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace AaxDecrypter { /// /// A resumable, simultaneous file downloader and reader. /// public class NetworkFileStream : Stream, IUpdatable { public event EventHandler Updated; #region Public Properties /// /// Location to save the downloaded data. /// [JsonProperty(Required = Required.Always)] public string SaveFilePath { get; } /// /// Http(s) address of the file to download. /// [JsonProperty(Required = Required.Always)] public Uri Uri { get; private set; } /// /// Http headers to be sent to the server with the request. /// [JsonProperty(Required = Required.Always)] public Dictionary RequestHeaders { get; private set; } /// /// The position in that has been written and flushed to disk. /// [JsonProperty(Required = Required.Always)] public long WritePosition { get; private set; } /// /// The total length of the file to download. /// [JsonProperty(Required = Required.Always)] public long ContentLength { get; private set; } [JsonIgnore] public bool IsCancelled { get; private set; } #endregion #region Private Properties private FileStream _writeFile { get; } private FileStream _readFile { get; } private EventWaitHandle _downloadedPiece { get; set; } private Task _backgroundDownloadTask { get; set; } #endregion #region Constants //Download buffer size private const int DOWNLOAD_BUFF_SZ = 32 * 1024; //NetworkFileStream will flush all data in _writeFile to disk after every //DATA_FLUSH_SZ bytes are written to the file stream. private const int DATA_FLUSH_SZ = 1024 * 1024; #endregion #region Constructor /// /// A resumable, simultaneous file downloader and reader. /// /// Path to a location on disk to save the downloaded data from /// Http(s) address of the file to download. /// The position in to begin downloading. /// Http headers to be sent to the server with the . /// A with cookies to send with the . It will also be populated with any cookies set by the server. public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, Dictionary requestHeaders = null) { ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath)); ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri)); ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1); if (!Directory.Exists(Path.GetDirectoryName(saveFilePath))) throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist."); SaveFilePath = saveFilePath; Uri = uri; WritePosition = writePosition; RequestHeaders = requestHeaders ?? new(); _writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite) { Position = WritePosition }; _readFile = new FileStream(SaveFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); SetUriForSameFile(uri); } #endregion #region Downloader /// /// Update the . /// private void Update() { RequestHeaders["Range"] = $"bytes={WritePosition}-"; try { Updated?.Invoke(this, EventArgs.Empty); } catch (Exception ex) { Serilog.Log.Error(ex, "An error was encountered while saving the download progress to JSON"); } } /// /// Set a different to the same file targeted by this instance of /// /// New host must match existing host. public void SetUriForSameFile(Uri uriToSameFile) { ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile)); 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}"); if (_backgroundDownloadTask is not null) throw new InvalidOperationException("Cannot change Uri after download has started."); Uri = uriToSameFile; RequestHeaders["Range"] = $"bytes={WritePosition}-"; } /// /// Begins downloading to in a background thread. /// private Task BeginDownloading() { if (ContentLength != 0 && WritePosition == ContentLength) return Task.CompletedTask; if (ContentLength != 0 && WritePosition > ContentLength) throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10})."); 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) throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}."); //Content length is the length of the range request, and it is only equal //to the complete file length if requesting Range: bytes=0- if (WritePosition == 0) ContentLength = response.Content.Headers.ContentLength.GetValueOrDefault(); var networkStream = response.Content.ReadAsStream(); _downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset); //Download the file in the background. return Task.Run(() => DownloadFile(networkStream)); } /// /// Download to . /// private void DownloadFile(Stream networkStream) { var downloadPosition = WritePosition; var nextFlush = downloadPosition + DATA_FLUSH_SZ; var buff = new byte[DOWNLOAD_BUFF_SZ]; try { int bytesRead; do { bytesRead = networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ); _writeFile.Write(buff, 0, bytesRead); downloadPosition += bytesRead; if (downloadPosition > nextFlush) { _writeFile.Flush(); WritePosition = downloadPosition; Update(); nextFlush = downloadPosition + DATA_FLUSH_SZ; _downloadedPiece.Set(); } } while (downloadPosition < ContentLength && !IsCancelled && bytesRead > 0); WritePosition = downloadPosition; if (!IsCancelled && WritePosition < ContentLength) throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10})."); if (WritePosition > ContentLength) throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10})."); } catch (Exception ex) { Serilog.Log.Error(ex, "An error was encountered while downloading {Uri}", Uri); } finally { networkStream.Close(); _writeFile.Close(); _downloadedPiece.Set(); Update(); } } #endregion #region Json Connverters public static JsonSerializerSettings GetJsonSerializerSettings() => new JsonSerializerSettings(); #endregion #region Download Stream Reader [JsonIgnore] public override bool CanRead => true; [JsonIgnore] public override bool CanSeek => true; [JsonIgnore] public override bool CanWrite => false; [JsonIgnore] public override long Length { get { _backgroundDownloadTask ??= BeginDownloading(); return ContentLength; } } [JsonIgnore] public override long Position { get => _readFile.Position; set => Seek(value, SeekOrigin.Begin); } [JsonIgnore] public override bool CanTimeout => false; [JsonIgnore] public override int ReadTimeout { get => base.ReadTimeout; set => base.ReadTimeout = value; } [JsonIgnore] public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; } public override void Flush() => throw new InvalidOperationException(); public override void SetLength(long value) => throw new InvalidOperationException(); public override void Write(byte[] buffer, int offset, int count) => throw new InvalidOperationException(); public override int Read(byte[] buffer, int offset, int count) { _backgroundDownloadTask ??= BeginDownloading(); var toRead = Math.Min(count, Length - Position); WaitToPosition(Position + toRead); return _readFile.Read(buffer, offset, count); } public override long Seek(long offset, SeekOrigin origin) { var newPosition = origin switch { SeekOrigin.Current => Position + offset, SeekOrigin.End => ContentLength + offset, _ => offset, }; WaitToPosition(newPosition); return _readFile.Position = newPosition; } /// /// Blocks until the file has downloaded to at least , then returns. /// /// The minimum required flished data length in . private void WaitToPosition(long requiredPosition) { while (WritePosition < requiredPosition && _backgroundDownloadTask?.IsCompleted is false && !IsCancelled) { _downloadedPiece.WaitOne(100); } } public override void Close() { IsCancelled = true; _backgroundDownloadTask?.Wait(); _readFile.Close(); _writeFile.Close(); Update(); } #endregion ~NetworkFileStream() { _downloadedPiece?.Close(); } } }