1
0
Fork 0
mirror of https://github.com/oMaN-Rod/nxDumpFuse.git synced 2024-11-08 11:51:49 +00:00

Add speed, progress, and clean up Fuse

This commit is contained in:
Omar 2021-10-29 13:35:20 -04:00
parent dbfff7fabe
commit a13cf7c091
8 changed files with 251 additions and 175 deletions

View file

@ -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<string> files)
{
long totalFileSize = 0;
files.Select(f => f).ToList().ForEach(f => totalFileSize += new FileInfo(f).Length);
return totalFileSize;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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<string> GetInputFiles(this string inputFilePath, FileCase fileCase)
{
var inputDir = Path.GetDirectoryName(inputFilePath);
if (string.IsNullOrEmpty(inputDir)) return new List<string>();
var files = new List<string>();
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<string,FileCase> 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<string, FileCase>(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<string, FileCase>(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<string, FileCase>(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<string, FileCase>(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<string, FileCase>(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<string, FileCase>(outputFilePath, FileCase.Numeric);
}
return new Tuple<string, FileCase>(string.Empty, FileCase.Invalid);
}
}
}

View file

@ -2,6 +2,7 @@
{
public enum FileCase
{
Invalid,
XciNumeric, // .xci.00
NspNumeric, // .nsp.00
Xci, // .xc0

View file

@ -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<string> inputFiles)
private async void FuseFiles(List<string> 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<string> inputFiles)
{
long totalFileSize = 0;
inputFiles.Select(f => f).ToList().ForEach(f => totalFileSize += new FileInfo(f).Length);
return totalFileSize;
}
private List<string> GetInputFiles()
{
var inputDir = Path.GetDirectoryName(_inputFilePath);
if (string.IsNullOrEmpty(inputDir)) return new List<string>();
var files = new List<string>();
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);
}
}
}

View file

@ -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; }
}
}

View file

@ -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<Unit, Unit> 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<FuseSimpleLog> _logItems = new();
private string _progressText = string.Empty;
public string ProgressText
{
get => _progressText;
set => this.RaiseAndSetIfChanged(ref _progressText, value);
}
private ObservableCollection<FuseSimpleLog> _logItems = new();
public ObservableCollection<FuseSimpleLog> 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)

View file

@ -8,64 +8,69 @@
x:DataType="vm:FuseViewModel">
<DockPanel LastChildFill="True">
<Grid Margin="20" DockPanel.Dock="Top"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto"
ColumnDefinitions="Auto,*">
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto"
ColumnDefinitions="Auto,*,50">
<Button Grid.Row="0" Grid.Column="0" Command="{Binding SelectInputFileCommand}" Content="Input"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" />
<TextBox Grid.Row="0" Grid.Column="1" Margin="5" VerticalAlignment="Center" Text="{Binding InputFilePath}"
<TextBox Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" Margin="5" VerticalAlignment="Center"
Text="{Binding InputFilePath}"
Name="InputFileTextBox" />
<Button Grid.Row="1" Grid.Column="0" Command="{Binding SelectOutputFolderCommand}" Content="Output"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" />
<TextBox Grid.Row="1" Grid.Column="1" Margin="5" VerticalAlignment="Center" Text="{Binding OutputDir}" />
<TextBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="5" VerticalAlignment="Center"
Text="{Binding OutputDir}" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding ProgressPartText}" HorizontalAlignment="Right"
Margin="2" />
<ProgressBar Grid.Row="2" Grid.Column="1" Margin="2" Height="10" Value="{Binding ProgressPart}" />
<ProgressBar Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="2" Height="10" Value="{Binding ProgressPart}" HorizontalAlignment="Stretch"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="Total" HorizontalAlignment="Right" Margin="2" />
<ProgressBar Grid.Row="3" Grid.Column="1" Margin="2" Height="10" Value="{Binding Progress}" />
<ProgressBar Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Margin="2" Height="10" Value="{Binding Progress}" HorizontalAlignment="Stretch"/>
<StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<StackPanel Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Command="{Binding FuseCommand}" Content="Fuse" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" Margin="2" />
<Button Command="{Binding StopCommand}" Content="Stop" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" Margin="2" />
</StackPanel>
<Expander Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2"
<Expander Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="3"
Header="Log"
Margin="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<DataGrid Grid.Column="0"
x:Name="FuseSimpleLog"
Items="{Binding LogItems}"
CanUserSortColumns="False"
CanUserResizeColumns="True"
CanUserReorderColumns="True"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
VerticalAlignment="Top"
Height="300"
LoadingRow="FuseSimpleLog_OnLoadingRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Type" Binding="{Binding Type}" Width="Auto"
Foreground="{Binding Color}" FontSize="12" />
<DataGridTextColumn Header="Time" Binding="{Binding Time}" Width="Auto" FontSize="12" />
<DataGridTextColumn Header="Message" Binding="{Binding Message}" Width="Auto" FontSize="12" />
</DataGrid.Columns>
</DataGrid>
<Button Grid.Column="1" Command="{Binding ClearLogCommand}" Content="Clear" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" VerticalAlignment="Top" Margin=" 4 0 0 0" />
</Grid>
<Grid ColumnDefinitions="*,Auto" >
<DataGrid Grid.Column="0"
x:Name="FuseSimpleLog"
Items="{Binding LogItems}"
CanUserSortColumns="False"
CanUserResizeColumns="True"
CanUserReorderColumns="True"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
VerticalAlignment="Top"
Height="300"
LoadingRow="FuseSimpleLog_OnLoadingRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Type" Binding="{Binding Type}" Width="Auto"
Foreground="{Binding Color}" FontSize="12" />
<DataGridTextColumn Header="Time" Binding="{Binding Time}" Width="Auto" FontSize="12" />
<DataGridTextColumn Header="Message" Binding="{Binding Message}" Width="Auto" FontSize="12" />
</DataGrid.Columns>
</DataGrid>
<Button Grid.Column="1" Command="{Binding ClearLogCommand}" Content="Clear"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" VerticalAlignment="Top" Margin="4 0 0 0" />
</Grid>
</Expander>
</Expander>
</Grid>
<TextBlock DockPanel.Dock="Bottom" Text="{Binding ProgressText}" HorizontalAlignment="Right" VerticalAlignment="Bottom" FontSize="12" Margin="0 0 20 0"/>
</DockPanel>
</UserControl>