From a13cf7c0917b4c577cd0189032be6b6773da60d9 Mon Sep 17 00:00:00 2001 From: Omar Date: Fri, 29 Oct 2021 13:35:20 -0400 Subject: [PATCH] Add speed, progress, and clean up Fuse --- src/nxDumpFuse/Extensions/ListExtensions.cs | 16 ++ src/nxDumpFuse/Extensions/LongExtensions.cs | 16 ++ src/nxDumpFuse/Extensions/StringExtensions.cs | 95 +++++++++ src/nxDumpFuse/Model/Enums/FileCase.cs | 1 + src/nxDumpFuse/Model/Fuse.cs | 182 +++++------------- src/nxDumpFuse/Model/FuseUpdateInfo.cs | 4 + src/nxDumpFuse/ViewModels/FuseViewModel.cs | 37 +++- src/nxDumpFuse/Views/FuseView.axaml | 75 ++++---- 8 files changed, 251 insertions(+), 175 deletions(-) create mode 100644 src/nxDumpFuse/Extensions/ListExtensions.cs create mode 100644 src/nxDumpFuse/Extensions/LongExtensions.cs create mode 100644 src/nxDumpFuse/Extensions/StringExtensions.cs diff --git a/src/nxDumpFuse/Extensions/ListExtensions.cs b/src/nxDumpFuse/Extensions/ListExtensions.cs new file mode 100644 index 0000000..eb12dfa --- /dev/null +++ b/src/nxDumpFuse/Extensions/ListExtensions.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace nxDumpFuse.Extensions +{ + public static class ListExtensions + { + public static long GetOutputFileSize(this List files) + { + long totalFileSize = 0; + files.Select(f => f).ToList().ForEach(f => totalFileSize += new FileInfo(f).Length); + return totalFileSize; + } + } +} diff --git a/src/nxDumpFuse/Extensions/LongExtensions.cs b/src/nxDumpFuse/Extensions/LongExtensions.cs new file mode 100644 index 0000000..70d1b0f --- /dev/null +++ b/src/nxDumpFuse/Extensions/LongExtensions.cs @@ -0,0 +1,16 @@ +namespace nxDumpFuse.Extensions +{ + public static class LongExtensions + { + public static long ToMb(this long bytes) + { + return bytes / (1024 * 1024); + } + + public static long ToSeconds(this long milliseconds) + { + return milliseconds / 1000; + } + + } +} diff --git a/src/nxDumpFuse/Extensions/StringExtensions.cs b/src/nxDumpFuse/Extensions/StringExtensions.cs new file mode 100644 index 0000000..080ce30 --- /dev/null +++ b/src/nxDumpFuse/Extensions/StringExtensions.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using nxDumpFuse.Model.Enums; + +namespace nxDumpFuse.Extensions +{ + public static class StringExtensions + { + public static List GetInputFiles(this string inputFilePath, FileCase fileCase) + { + var inputDir = Path.GetDirectoryName(inputFilePath); + if (string.IsNullOrEmpty(inputDir)) return new List(); + var files = new List(); + switch (fileCase) + { + case FileCase.XciNumeric: // .xci.00 + case FileCase.NspNumeric: // .nsp.00 + files = Directory.GetFiles(inputDir) + .Where(f => int.TryParse(Path.GetExtension(f).Replace(".", ""), out _)) + .ToList(); + break; + case FileCase.Xci: // .xc0 + case FileCase.Nsp: // .ns0 + files = Directory.GetFiles(inputDir, $"{Path.GetFileNameWithoutExtension(inputFilePath)}*") + .ToList(); + break; + case FileCase.Numeric: // dir/00 + files = Directory.GetFiles(inputDir) + .Where(f => int.TryParse(Path.GetFileName(f), out _)) + .ToList(); + break; + } + files.Sort(); + return files; + } + + public static Tuple GetOutputFilePath(this string inputFilePath, string outputDir) + { + var fileName = Path.GetFileName(inputFilePath); + string outputFilePath; + const string xciExt = "xci"; + const string nspExt = "nsp"; + + if (Path.HasExtension(fileName)) + { + var ext = Path.GetExtension(fileName).Replace(".", string.Empty); + var split = fileName.Split(".").ToList(); + + if (int.TryParse(ext, out _) && split.Count >= 3 && split[^2] == xciExt) // .xci.00 + { + outputFilePath = Path.Join(outputDir, $"{string.Join("", split.Take(split.Count - 2))}.{xciExt}"); + return new Tuple(outputFilePath, FileCase.XciNumeric); + } + + if (int.TryParse(ext, out _) && split.Count >= 3 && split[^2] == nspExt) // .nsp.00 + { + outputFilePath = Path.Join(outputDir, $"{string.Join("", split.Take(split.Count - 2))}.{nspExt}"); + return new Tuple(outputFilePath, FileCase.NspNumeric); + } + + switch (ext[..2]) + { + // .xc0 + case "xc" when int.TryParse(ext.Substring(ext.Length - 1, 1), out _): + outputFilePath = Path.Join(outputDir, $"{Path.GetFileNameWithoutExtension(fileName)}.{xciExt}"); + return new Tuple(outputFilePath, FileCase.Xci); + // .ns0 + case "ns" when int.TryParse(ext.Substring(ext.Length - 1, 1), out _): + outputFilePath = Path.Join(outputDir, $"{Path.GetFileNameWithoutExtension(fileName)}.{nspExt}"); + return new Tuple(outputFilePath, FileCase.Nsp); + } + } + else // dir/00 + { + var inputDir = new FileInfo(inputFilePath).Directory?.Name; + if (string.IsNullOrEmpty(inputDir)) + { + inputDir = Path.GetPathRoot(inputFilePath); + outputFilePath = $"{inputDir}.{nspExt}"; + return new Tuple(outputFilePath, FileCase.Numeric); + } + + var inputDirSplit = inputDir.Split("."); + outputFilePath = Path.Join(outputDir, inputDirSplit.Length == 1 + ? $"{inputDir}.{nspExt}" + : $"{string.Join("", (inputDirSplit).Take(inputDirSplit.Length - 1))}.{nspExt}"); + return new Tuple(outputFilePath, FileCase.Numeric); + } + + return new Tuple(string.Empty, FileCase.Invalid); + } + } +} diff --git a/src/nxDumpFuse/Model/Enums/FileCase.cs b/src/nxDumpFuse/Model/Enums/FileCase.cs index 5057633..e4c7b11 100644 --- a/src/nxDumpFuse/Model/Enums/FileCase.cs +++ b/src/nxDumpFuse/Model/Enums/FileCase.cs @@ -2,6 +2,7 @@ { public enum FileCase { + Invalid, XciNumeric, // .xci.00 NspNumeric, // .nsp.00 Xci, // .xc0 diff --git a/src/nxDumpFuse/Model/Fuse.cs b/src/nxDumpFuse/Model/Fuse.cs index 813022a..c2bebbe 100644 --- a/src/nxDumpFuse/Model/Fuse.cs +++ b/src/nxDumpFuse/Model/Fuse.cs @@ -2,23 +2,23 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using nxDumpFuse.Events; +using nxDumpFuse.Extensions; using nxDumpFuse.Model.Enums; namespace nxDumpFuse.Model { public class Fuse { - private const string XciExt = "xci"; - private const string NspExt = "nsp"; + private readonly CancellationTokenSource _cts; private readonly string _inputFilePath; private readonly string _outputDir; private string _outputFilePath = string.Empty; private FileCase _fileCase; + private readonly Stopwatch _sw = new(); public Fuse(string inputFilePath, string outputDir) { @@ -35,14 +35,16 @@ namespace nxDumpFuse.Model FuseUpdateEvent?.Invoke(fuseUpdateInfo); } - private void Update(int part, int parts, double progress, double progressPart) + private void Update(int part, int parts, double progress, double progressPart, long speed, bool complete = false) { OnFuseUpdate(new FuseUpdateInfo { Part = part, Parts = parts, Progress = progress, - ProgressPart = progressPart + ProgressPart = progressPart, + Speed = speed.ToMb(), + Complete = complete }); } @@ -56,7 +58,7 @@ namespace nxDumpFuse.Model OnFuseSimpleLogEvent(new FuseSimpleLog(type, DateTime.Now, message)); } - public void FuseDump() + public void Start() { if (string.IsNullOrEmpty(_inputFilePath)) { @@ -69,14 +71,14 @@ namespace nxDumpFuse.Model return; } - GetOutputFilePath(); - if (string.IsNullOrEmpty(_outputFilePath)) + (_outputFilePath, _fileCase) = _inputFilePath.GetOutputFilePath(_outputDir); + if (string.IsNullOrEmpty(_outputFilePath) || _fileCase == FileCase.Invalid) { Log(FuseSimpleLogType.Error, "Output path was null"); return; } - var inputFiles = GetInputFiles(); + var inputFiles = _inputFilePath.GetInputFiles(_fileCase); if (inputFiles.Count == 0) { Log(FuseSimpleLogType.Error, "No input files found"); @@ -86,77 +88,58 @@ namespace nxDumpFuse.Model FuseFiles(inputFiles); } - private void GetOutputFilePath() + public void Stop() { - - var fileName = Path.GetFileName(_inputFilePath); - if (Path.HasExtension(fileName)) - { - var ext = Path.GetExtension(fileName).Replace(".", string.Empty); - var split = fileName.Split(".").ToList(); + _cts.Cancel(); + _sw.Stop(); - if (int.TryParse(ext, out _) && split.Count >= 3 && split[^2] == XciExt) // .xci.00 - { - _outputFilePath = Path.Join(_outputDir, $"{string.Join("", split.Take(split.Count - 2))}.{XciExt}"); - _fileCase = FileCase.XciNumeric; - } - else if (int.TryParse(ext, out _) && split.Count >= 3 && split[^2] == NspExt) // .nsp.00 - { - _outputFilePath = Path.Join(_outputDir, $"{string.Join("", split.Take(split.Count - 2))}.{NspExt}"); - _fileCase = FileCase.NspNumeric; - } - else switch (ext[..2]) - { - // .xc0 - case "xc" when int.TryParse(ext.Substring(ext.Length - 1, 1), out _): - _outputFilePath = Path.Join(_outputDir, $"{Path.GetFileNameWithoutExtension(fileName)}.{XciExt}"); - _fileCase = FileCase.Xci; - break; - // .ns0 - case "ns" when int.TryParse(ext.Substring(ext.Length - 1, 1), out _): - _outputFilePath = Path.Join(_outputDir, $"{Path.GetFileNameWithoutExtension(fileName)}.{NspExt}"); - _fileCase = FileCase.Nsp; - break; - } - } - else // dir/00 - { - _fileCase = FileCase.Numeric; - var inputDir = new FileInfo(_inputFilePath).Directory?.Name; - if (string.IsNullOrEmpty(inputDir)) - { - inputDir = Path.GetPathRoot(_inputFilePath); - _outputFilePath = $"{inputDir}.{NspExt}"; - return; - } + Log(FuseSimpleLogType.Information, "Fuse Stopped"); - var inputDirSplit = inputDir.Split("."); - _outputFilePath = Path.Join(_outputDir, inputDirSplit.Length == 1 - ? $"{inputDir}.{NspExt}" - : $"{string.Join("", (inputDirSplit).Take(inputDirSplit.Length - 1))}.{NspExt}"); + if (File.Exists(_outputFilePath)) + { + Task.Run((() => + { + const int retries = 5; + for (var i = 0; i <= retries; i++) + { + try + { + File.Delete(_outputFilePath); + Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => Log(FuseSimpleLogType.Information, $"Deleted {_outputFilePath}")); + Update(0, 0, 0, 0, 0); + break; + } + catch (IOException) + { + Thread.Sleep(1000); + } + } + })); } } - private async void FuseFiles(IReadOnlyCollection inputFiles) + private async void FuseFiles(List inputFiles) { var buffer = new byte[1024 * 1024]; var count = 0; long totalBytes = 0; - var totalFileLength = GetTotalFileSize(inputFiles); + var totalFileLength = inputFiles.GetOutputFileSize(); - Log(FuseSimpleLogType.Information, $"Fusing {inputFiles.Count} parts to {_outputFilePath} ({ToMb(totalFileLength)}MB)"); - + Log(FuseSimpleLogType.Information, $"Fusing {inputFiles.Count} parts to {_outputFilePath} ({totalFileLength.ToMb()}MB)"); + + _sw.Start(); await using var outputStream = File.Create(_outputFilePath); foreach (var inputFilePath in inputFiles) { if (_cts.Token.IsCancellationRequested) return; long currentBytes = 0; int currentBlockSize; + long copySpeed = 0; await using var inputStream = File.OpenRead(inputFilePath); var fileLength = inputStream.Length; - Log(FuseSimpleLogType.Information, $"Fusing file part {++count}-> {inputFilePath} ({ToMb(fileLength)}MB)"); + Log(FuseSimpleLogType.Information, $"Fusing file part {++count}-> {inputFilePath} ({fileLength.ToMb()}MB)"); while ((currentBlockSize = inputStream.Read(buffer, 0, buffer.Length)) > 0) { @@ -172,84 +155,21 @@ namespace nxDumpFuse.Model catch (TaskCanceledException e) { Log(FuseSimpleLogType.Error, e.Message); + _sw.Stop(); + Update(0,0,0,0,0,true); + return; } var progress = totalBytes * 100.0 / totalFileLength; var progressPart = currentBytes * 100.0 / fileLength; - Update(count, inputFiles.Count, progress, progressPart); + if(_sw.ElapsedMilliseconds >= 1000) copySpeed = totalBytes / _sw.ElapsedMilliseconds.ToSeconds(); + Update(count, inputFiles.Count, progress, progressPart, copySpeed); } } - - Log(FuseSimpleLogType.Information, "Fuse Complete"); - } - - private static long ToMb(long bytes) - { - return bytes / 1000000; - } - - private static long GetTotalFileSize(IEnumerable inputFiles) - { - long totalFileSize = 0; - inputFiles.Select(f => f).ToList().ForEach(f => totalFileSize += new FileInfo(f).Length); - return totalFileSize; - } - - private List GetInputFiles() - { - var inputDir = Path.GetDirectoryName(_inputFilePath); - if (string.IsNullOrEmpty(inputDir)) return new List(); - var files = new List(); - switch (_fileCase) - { - case FileCase.XciNumeric: // .xci.00 - case FileCase.NspNumeric: // .nsp.00 - files = Directory.GetFiles(inputDir) - .Where(f => int.TryParse(Path.GetExtension(f).Replace(".", ""), out _)) - .ToList(); - break; - case FileCase.Xci: // .xc0 - case FileCase.Nsp: // .ns0 - files = Directory.GetFiles(inputDir, $"{Path.GetFileNameWithoutExtension(_inputFilePath)}*") - .ToList(); - break; - case FileCase.Numeric: // dir/00 - files = Directory.GetFiles(inputDir) - .Where(f => int.TryParse(Path.GetFileName(f), out _)) - .ToList(); - break; - } - files.Sort(); - return files; - } - - public void StopFuse() - { - _cts.Cancel(); - - Log(FuseSimpleLogType.Information, "Fuse Stopped"); - - if (File.Exists(_outputFilePath)) - { - Task.Run((() => - { - const int retries = 5; - for (var i = 0; i <= retries; i++) - { - try - { - File.Delete(_outputFilePath); - Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => Log(FuseSimpleLogType.Information, $"Deleted {_outputFilePath}")); - Update(0, 0, 0, 0); - break; - } - catch (IOException) - { - Thread.Sleep(1000); - } - } - })); - } + + Log(FuseSimpleLogType.Information, $"Fuse Completed in {_sw.ElapsedMilliseconds.ToSeconds()}s"); + _sw.Stop(); + Update(0, 0, 0, 0, 0, true); } } } \ No newline at end of file diff --git a/src/nxDumpFuse/Model/FuseUpdateInfo.cs b/src/nxDumpFuse/Model/FuseUpdateInfo.cs index e5b63d0..d7ae5dc 100644 --- a/src/nxDumpFuse/Model/FuseUpdateInfo.cs +++ b/src/nxDumpFuse/Model/FuseUpdateInfo.cs @@ -6,8 +6,12 @@ public double ProgressPart { get; set; } + public long Speed { get; set; } + public int Part { get; set; } public int Parts { get; set; } + + public bool Complete { get; set; } } } diff --git a/src/nxDumpFuse/ViewModels/FuseViewModel.cs b/src/nxDumpFuse/ViewModels/FuseViewModel.cs index 84dee2d..4e0c530 100644 --- a/src/nxDumpFuse/ViewModels/FuseViewModel.cs +++ b/src/nxDumpFuse/ViewModels/FuseViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Reactive; using Avalonia.Controls; using nxDumpFuse.Interfaces; @@ -14,6 +15,8 @@ namespace nxDumpFuse.ViewModels { private readonly IDialogService _dialogService; private Fuse? _fuse; + private readonly Stopwatch _sw = new(); + private TimeSpan _elapsed; public FuseViewModel(IDialogService dialogService) { @@ -37,7 +40,6 @@ namespace nxDumpFuse.ViewModels public ReactiveCommand StopCommand { get; } private string _inputFilePath = string.Empty; - public string InputFilePath { get => _inputFilePath; @@ -45,7 +47,6 @@ namespace nxDumpFuse.ViewModels } private string _outputDir = string.Empty; - public string OutputDir { get => _outputDir; @@ -53,7 +54,6 @@ namespace nxDumpFuse.ViewModels } private string _progressPartText = string.Empty; - public string ProgressPartText { get => _progressPartText; @@ -61,7 +61,6 @@ namespace nxDumpFuse.ViewModels } private double _progressPart; - public double ProgressPart { get => _progressPart; @@ -69,15 +68,20 @@ namespace nxDumpFuse.ViewModels } private double _progress; - public double Progress { get => _progress; set => this.RaiseAndSetIfChanged(ref _progress, value); } - private ObservableCollection _logItems = new(); + private string _progressText = string.Empty; + public string ProgressText + { + get => _progressText; + set => this.RaiseAndSetIfChanged(ref _progressText, value); + } + private ObservableCollection _logItems = new(); public ObservableCollection LogItems { get => _logItems; @@ -99,18 +103,22 @@ namespace nxDumpFuse.ViewModels _fuse = new Fuse(InputFilePath, OutputDir); _fuse.FuseUpdateEvent += OnFuseUpdate; _fuse.FuseSimpleLogEvent += OnFuseSimpleLogEvent; + _sw.Start(); try { - _fuse.FuseDump(); + _fuse.Start(); } catch (Exception e) { + _sw.Stop(); OnFuseSimpleLogEvent(new FuseSimpleLog(FuseSimpleLogType.Error, DateTime.Now, e.Message)); } } private void StopDump() { - _fuse?.StopFuse(); + _sw.Stop(); + _fuse?.Stop(); + ProgressText = string.Empty; } private void ClearLog() @@ -120,9 +128,20 @@ namespace nxDumpFuse.ViewModels private void OnFuseUpdate(FuseUpdateInfo fuseUpdateInfo) { + if (fuseUpdateInfo.Complete) + { + _sw.Stop(); + ProgressText = string.Empty; + return; + } ProgressPart = fuseUpdateInfo.ProgressPart; - Progress = fuseUpdateInfo.Progress; ProgressPartText = $"Part {fuseUpdateInfo.Part}/{fuseUpdateInfo.Parts}"; + Progress = fuseUpdateInfo.Progress; + + if (!(_sw.Elapsed.TotalSeconds >= 0.5 && + _sw.Elapsed.TotalSeconds - _elapsed.TotalSeconds >= 0.5)) return; + _elapsed = _sw.Elapsed; + ProgressText = $"({fuseUpdateInfo.Speed:0}MB/s) {Progress:0}% "; } private void OnFuseSimpleLogEvent(FuseSimpleLog log) diff --git a/src/nxDumpFuse/Views/FuseView.axaml b/src/nxDumpFuse/Views/FuseView.axaml index ead28cd..a018bf0 100644 --- a/src/nxDumpFuse/Views/FuseView.axaml +++ b/src/nxDumpFuse/Views/FuseView.axaml @@ -8,64 +8,69 @@ x:DataType="vm:FuseViewModel"> + RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto" + ColumnDefinitions="Auto,*,50">