Ionite
1 year ago
15 changed files with 680 additions and 86 deletions
@ -0,0 +1,65 @@
|
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
|
||||
namespace StabilityMatrix.Avalonia.DesignData; |
||||
|
||||
public class MockDownloadProgressItemViewModel : PausableProgressItemViewModelBase |
||||
{ |
||||
private Task? dummyTask; |
||||
private CancellationTokenSource? cts; |
||||
|
||||
public MockDownloadProgressItemViewModel(string fileName) |
||||
{ |
||||
Name = fileName; |
||||
Progress.Value = 5; |
||||
Progress.IsIndeterminate = false; |
||||
Progress.Text = "Downloading..."; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task Cancel() |
||||
{ |
||||
// Cancel the task that updates progress |
||||
cts?.Cancel(); |
||||
cts = null; |
||||
dummyTask = null; |
||||
|
||||
State = ProgressState.Cancelled; |
||||
Progress.Text = "Cancelled"; |
||||
return Task.CompletedTask; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task Pause() |
||||
{ |
||||
// Cancel the task that updates progress |
||||
cts?.Cancel(); |
||||
cts = null; |
||||
dummyTask = null; |
||||
|
||||
State = ProgressState.Inactive; |
||||
|
||||
return Task.CompletedTask; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task Resume() |
||||
{ |
||||
// Start a task that updates progress every 100ms |
||||
cts = new CancellationTokenSource(); |
||||
dummyTask = Task.Run(async () => |
||||
{ |
||||
while (State != ProgressState.Success) |
||||
{ |
||||
await Task.Delay(100, cts.Token); |
||||
Progress.Value += 1; |
||||
} |
||||
}, cts.Token); |
||||
|
||||
State = ProgressState.Working; |
||||
|
||||
return Task.CompletedTask; |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
using System; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.DesignData; |
||||
|
||||
public class MockTrackedDownloadService : ITrackedDownloadService |
||||
{ |
||||
/// <inheritdoc /> |
||||
public event EventHandler<TrackedDownload>? DownloadAdded; |
||||
|
||||
/// <inheritdoc /> |
||||
public TrackedDownload NewDownload(Uri downloadUrl, FilePath downloadPath) |
||||
{ |
||||
throw new NotImplementedException(); |
||||
} |
||||
} |
@ -0,0 +1,49 @@
|
||||
using System.Diagnostics.CodeAnalysis; |
||||
using System.Threading.Tasks; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.Base; |
||||
|
||||
[SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")] |
||||
public abstract partial class PausableProgressItemViewModelBase : ProgressItemViewModelBase |
||||
{ |
||||
[ObservableProperty] |
||||
[NotifyPropertyChangedFor(nameof(IsPaused), nameof(IsCompleted), nameof(CanPauseResume), nameof(CanCancel))] |
||||
private ProgressState state = ProgressState.Inactive; |
||||
|
||||
/// <summary> |
||||
/// Whether the progress is paused |
||||
/// </summary> |
||||
public bool IsPaused => State == ProgressState.Inactive; |
||||
|
||||
/// <summary> |
||||
/// Whether the progress has succeeded, failed or was cancelled |
||||
/// </summary> |
||||
public override bool IsCompleted => State is ProgressState.Success or ProgressState.Failed or ProgressState.Cancelled; |
||||
|
||||
public virtual bool SupportsPauseResume => true; |
||||
public virtual bool SupportsCancel => true; |
||||
|
||||
public bool CanPauseResume => SupportsPauseResume && !IsCompleted; |
||||
public bool CanCancel => SupportsCancel && !IsCompleted; |
||||
|
||||
private AsyncRelayCommand? pauseCommand; |
||||
public IAsyncRelayCommand PauseCommand => pauseCommand ??= new AsyncRelayCommand(Pause); |
||||
public virtual Task Pause() => Task.CompletedTask; |
||||
|
||||
private AsyncRelayCommand? resumeCommand; |
||||
public IAsyncRelayCommand ResumeCommand => resumeCommand ??= new AsyncRelayCommand(Resume); |
||||
public virtual Task Resume() => Task.CompletedTask; |
||||
|
||||
private AsyncRelayCommand? cancelCommand; |
||||
public IAsyncRelayCommand CancelCommand => cancelCommand ??= new AsyncRelayCommand(Cancel); |
||||
public virtual Task Cancel() => Task.CompletedTask; |
||||
|
||||
[RelayCommand] |
||||
private Task TogglePauseResume() |
||||
{ |
||||
return IsPaused ? Resume() : Pause(); |
||||
} |
||||
} |
@ -0,0 +1,16 @@
|
||||
using System; |
||||
using System.Threading.Tasks; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.Base; |
||||
|
||||
public abstract partial class ProgressItemViewModelBase : ViewModelBase |
||||
{ |
||||
[ObservableProperty] private Guid id; |
||||
[ObservableProperty] private string? name; |
||||
[ObservableProperty] private bool failed; |
||||
|
||||
public virtual bool IsCompleted => Progress.Value >= 100 || Failed; |
||||
|
||||
public ProgressViewModel Progress { get; } = new(); |
||||
} |
@ -0,0 +1,73 @@
|
||||
using System.Threading.Tasks; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels; |
||||
|
||||
public class DownloadProgressItemViewModel : PausableProgressItemViewModelBase |
||||
{ |
||||
private readonly TrackedDownload download; |
||||
|
||||
/// <inheritdoc /> |
||||
public override bool SupportsPauseResume => true; |
||||
|
||||
public DownloadProgressItemViewModel(TrackedDownload download) |
||||
{ |
||||
this.download = download; |
||||
|
||||
download.ProgressUpdate += (s, e) => |
||||
{ |
||||
Progress.Value = e.Percentage; |
||||
Progress.IsIndeterminate = e.IsIndeterminate; |
||||
Progress.Text = e.Title; |
||||
}; |
||||
|
||||
download.ProgressStateChanged += (s, e) => |
||||
{ |
||||
State = e; |
||||
|
||||
if (e == ProgressState.Inactive) |
||||
{ |
||||
Progress.Text = "Paused"; |
||||
} |
||||
else if (e == ProgressState.Working) |
||||
{ |
||||
Progress.Text = "Downloading..."; |
||||
} |
||||
else if (e == ProgressState.Success) |
||||
{ |
||||
Progress.Text = "Completed"; |
||||
} |
||||
else if (e == ProgressState.Cancelled) |
||||
{ |
||||
Progress.Text = "Cancelled"; |
||||
} |
||||
else if (e == ProgressState.Failed) |
||||
{ |
||||
Progress.Text = "Failed"; |
||||
} |
||||
}; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task Cancel() |
||||
{ |
||||
download.Cancel(); |
||||
return Task.CompletedTask; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task Pause() |
||||
{ |
||||
download.Pause(); |
||||
return Task.CompletedTask; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task Resume() |
||||
{ |
||||
download.Resume(); |
||||
return Task.CompletedTask; |
||||
} |
||||
} |
@ -0,0 +1,245 @@
|
||||
using System.Diagnostics.CodeAnalysis; |
||||
using System.Text.Json.Serialization; |
||||
using AsyncAwaitBestPractices; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Core.Models; |
||||
|
||||
public class TrackedDownloadProgressEventArgs : EventArgs |
||||
{ |
||||
public ProgressReport Progress { get; init; } |
||||
public ProgressState State { get; init; } |
||||
} |
||||
|
||||
public class TrackedDownload |
||||
{ |
||||
[JsonIgnore] |
||||
private IDownloadService? downloadService; |
||||
|
||||
[JsonIgnore] |
||||
private Task? downloadTask; |
||||
|
||||
[JsonIgnore] |
||||
private CancellationTokenSource? downloadCancellationTokenSource; |
||||
|
||||
[JsonIgnore] |
||||
private CancellationTokenSource? downloadPauseTokenSource; |
||||
|
||||
private CancellationTokenSource AggregateCancellationTokenSource => |
||||
CancellationTokenSource.CreateLinkedTokenSource( |
||||
downloadCancellationTokenSource?.Token ?? CancellationToken.None, |
||||
downloadPauseTokenSource?.Token ?? CancellationToken.None); |
||||
|
||||
public required Guid Id { get; init; } |
||||
|
||||
public required Uri SourceUrl { get; init; } |
||||
|
||||
public Uri? RedirectedUrl { get; init; } |
||||
|
||||
public required DirectoryPath DownloadDirectory { get; init; } |
||||
|
||||
public required string FileName { get; init; } |
||||
|
||||
public required string TempFileName { get; init; } |
||||
|
||||
public string? ExpectedHashSha256 { get; init; } |
||||
|
||||
public bool ValidateHash { get; init; } |
||||
|
||||
public ProgressState ProgressState { get; private set; } = ProgressState.Inactive; |
||||
|
||||
public Exception? Exception { get; private set; } |
||||
|
||||
#region Events |
||||
private WeakEventManager<ProgressReport>? progressUpdateEventManager; |
||||
|
||||
public event EventHandler<ProgressReport> ProgressUpdate |
||||
{ |
||||
add |
||||
{ |
||||
progressUpdateEventManager ??= new WeakEventManager<ProgressReport>(); |
||||
progressUpdateEventManager.AddEventHandler(value); |
||||
} |
||||
remove => progressUpdateEventManager?.RemoveEventHandler(value); |
||||
} |
||||
|
||||
protected void OnProgressUpdate(ProgressReport e) |
||||
{ |
||||
progressUpdateEventManager?.RaiseEvent(this, e, nameof(ProgressUpdate)); |
||||
} |
||||
|
||||
private WeakEventManager<ProgressState>? progressStateChangedEventManager; |
||||
|
||||
public event EventHandler<ProgressState> ProgressStateChanged |
||||
{ |
||||
add |
||||
{ |
||||
progressStateChangedEventManager ??= new WeakEventManager<ProgressState>(); |
||||
progressStateChangedEventManager.AddEventHandler(value); |
||||
} |
||||
remove => progressStateChangedEventManager?.RemoveEventHandler(value); |
||||
} |
||||
|
||||
protected void OnProgressStateChanged(ProgressState e) |
||||
{ |
||||
progressStateChangedEventManager?.RaiseEvent(this, e, nameof(ProgressStateChanged)); |
||||
} |
||||
#endregion |
||||
|
||||
[MemberNotNull(nameof(downloadService))] |
||||
private void EnsureDownloadService() |
||||
{ |
||||
if (downloadService == null) |
||||
{ |
||||
throw new InvalidOperationException("Download service is not set."); |
||||
} |
||||
} |
||||
|
||||
private async Task StartDownloadTask(long resumeFromByte, CancellationToken cancellationToken) |
||||
{ |
||||
var progress = new Progress<ProgressReport>(OnProgressUpdate); |
||||
|
||||
await downloadService!.ResumeDownloadToFileAsync( |
||||
SourceUrl.ToString(), |
||||
DownloadDirectory.JoinFile(TempFileName), |
||||
resumeFromByte, |
||||
progress, |
||||
cancellationToken: cancellationToken).ConfigureAwait(false); |
||||
|
||||
// If hash validation is enabled, validate the hash |
||||
if (ValidateHash) |
||||
{ |
||||
var hash = await FileHash.GetSha256Async(DownloadDirectory.JoinFile(TempFileName), progress).ConfigureAwait(false); |
||||
if (hash != ExpectedHashSha256) |
||||
{ |
||||
throw new Exception($"Hash validation for {FileName} failed, expected {ExpectedHashSha256} but got {hash}"); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public void Start() |
||||
{ |
||||
if (ProgressState != ProgressState.Inactive) |
||||
{ |
||||
throw new InvalidOperationException($"Download state must be inactive to start, not {ProgressState}"); |
||||
} |
||||
|
||||
EnsureDownloadService(); |
||||
|
||||
downloadCancellationTokenSource = new CancellationTokenSource(); |
||||
downloadPauseTokenSource = new CancellationTokenSource(); |
||||
|
||||
downloadTask = StartDownloadTask(0, AggregateCancellationTokenSource.Token) |
||||
.ContinueWith(OnDownloadTaskCompleted); |
||||
} |
||||
|
||||
public void Resume() |
||||
{ |
||||
if (ProgressState != ProgressState.Inactive) return; |
||||
|
||||
EnsureDownloadService(); |
||||
|
||||
downloadCancellationTokenSource = new CancellationTokenSource(); |
||||
downloadPauseTokenSource = new CancellationTokenSource(); |
||||
|
||||
downloadTask = StartDownloadTask(0, AggregateCancellationTokenSource.Token) |
||||
.ContinueWith(OnDownloadTaskCompleted); |
||||
} |
||||
|
||||
public void Pause() |
||||
{ |
||||
if (ProgressState != ProgressState.Working) return; |
||||
|
||||
downloadPauseTokenSource?.Cancel(); |
||||
} |
||||
|
||||
public void Cancel() |
||||
{ |
||||
if (ProgressState is not (ProgressState.Working or ProgressState.Inactive)) return; |
||||
|
||||
downloadCancellationTokenSource?.Cancel(); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Invoked by the task's completion callback |
||||
/// </summary> |
||||
private void OnDownloadTaskCompleted(Task task) |
||||
{ |
||||
// For cancelled, check if it was actually cancelled or paused |
||||
if (task.IsCanceled) |
||||
{ |
||||
// If the task was cancelled, set the state to cancelled |
||||
if (downloadCancellationTokenSource?.IsCancellationRequested == true) |
||||
{ |
||||
ProgressState = ProgressState.Cancelled; |
||||
} |
||||
// If the task was not cancelled, set the state to paused |
||||
else if (downloadPauseTokenSource?.IsCancellationRequested == true) |
||||
{ |
||||
ProgressState = ProgressState.Inactive; |
||||
} |
||||
else |
||||
{ |
||||
throw new InvalidOperationException("Download task was cancelled but neither cancellation token was cancelled."); |
||||
} |
||||
} |
||||
// For faulted |
||||
else if (task.IsFaulted) |
||||
{ |
||||
// Set the exception |
||||
Exception = task.Exception; |
||||
|
||||
// Delete the temp file |
||||
try |
||||
{ |
||||
DownloadDirectory.JoinFile(TempFileName).Delete(); |
||||
} |
||||
catch (IOException) |
||||
{ |
||||
} |
||||
|
||||
ProgressState = ProgressState.Failed; |
||||
} |
||||
// Otherwise success |
||||
else |
||||
{ |
||||
ProgressState = ProgressState.Success; |
||||
} |
||||
|
||||
// For failed or cancelled, delete the temp file |
||||
if (ProgressState is ProgressState.Failed or ProgressState.Cancelled) |
||||
{ |
||||
// Delete the temp file |
||||
try |
||||
{ |
||||
DownloadDirectory.JoinFile(TempFileName).Delete(); |
||||
} |
||||
catch (IOException) |
||||
{ |
||||
} |
||||
} |
||||
else if (ProgressState == ProgressState.Success) |
||||
{ |
||||
// Move the temp file to the final file |
||||
DownloadDirectory.JoinFile(TempFileName).MoveTo(DownloadDirectory.JoinFile(FileName)); |
||||
} |
||||
|
||||
// For pause, just do nothing |
||||
|
||||
OnProgressStateChanged(ProgressState); |
||||
|
||||
// Dispose of the task and cancellation token |
||||
downloadTask?.Dispose(); |
||||
downloadTask = null; |
||||
downloadCancellationTokenSource?.Dispose(); |
||||
downloadCancellationTokenSource = null; |
||||
} |
||||
|
||||
public void SetDownloadService(IDownloadService service) |
||||
{ |
||||
downloadService = service; |
||||
} |
||||
} |
Loading…
Reference in new issue