// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // // Filename : MediaDumpViewModel.cs // Author(s) : Natalia Portillo // // Component : GUI view models. // // --[ Description ] ---------------------------------------------------------- // // View model and code for the media dump window. // // --[ License ] -------------------------------------------------------------- // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General public License for more details. // // You should have received a copy of the GNU General public License // along with this program. If not, see . // // ---------------------------------------------------------------------------- // Copyright © 2011-2022 Natalia Portillo // ****************************************************************************/ namespace Aaru.Gui.ViewModels.Windows; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Reactive; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Serialization; using Aaru.CommonTypes; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; using Aaru.CommonTypes.Metadata; using Aaru.Core; using Aaru.Core.Devices.Dumping; using Aaru.Core.Logging; using Aaru.Core.Media.Info; using Aaru.Devices; using Aaru.Gui.Models; using Avalonia.Controls; using Avalonia.Threading; using DynamicData; using JetBrains.Annotations; using MessageBox.Avalonia; using MessageBox.Avalonia.Enums; using ReactiveUI; using Schemas; using DeviceInfo = Aaru.Core.Devices.Info.DeviceInfo; using MediaType = Aaru.CommonTypes.MediaType; public sealed class MediaDumpViewModel : ViewModelBase { readonly string _devicePath; readonly Window _view; bool _closeVisible; string _destination; bool _destinationEnabled; Device _dev; Dump _dumper; string _encodingEnabled; bool _encodingVisible; bool _existingMetadata; bool _force; string _formatReadOnly; string _log; bool _optionsVisible; string _outputPrefix; bool _persistent; bool _progress1Visible; bool _progress2Indeterminate; double _progress2MaxValue; string _progress2Text; double _progress2Value; bool _progress2Visible; bool _progressIndeterminate; double _progressMaxValue; string _progressText; double _progressValue; bool _progressVisible; Resume _resume; double _retries; EncodingModel _selectedEncoding; ImagePluginModel _selectedPlugin; CICMMetadataType _sidecar; double _skipped; bool _startVisible; bool _stopEnabled; bool _stopOnError; bool _stopVisible; bool _track1Pregap; bool _track1PregapVisible; bool _trim; bool _useResume; bool _useSidecar; public MediaDumpViewModel(string devicePath, DeviceInfo deviceInfo, Window view, [CanBeNull] ScsiInfo scsiInfo = null) { _view = view; DestinationEnabled = true; StartVisible = true; CloseVisible = true; OptionsVisible = true; StartCommand = ReactiveCommand.Create(ExecuteStartCommand); CloseCommand = ReactiveCommand.Create(ExecuteCloseCommand); StopCommand = ReactiveCommand.Create(ExecuteStopCommand); DestinationCommand = ReactiveCommand.Create(ExecuteDestinationCommand); PluginsList = new ObservableCollection(); Encodings = new ObservableCollection(); // Defaults StopOnError = false; Force = false; Persistent = true; Resume = true; Track1Pregap = false; Sidecar = true; Trim = true; ExistingMetadata = false; Retries = 5; Skipped = 512; MediaType mediaType; if(scsiInfo != null) mediaType = scsiInfo.MediaType; else switch(deviceInfo.Type) { case DeviceType.SecureDigital: mediaType = MediaType.SecureDigital; break; case DeviceType.MMC: mediaType = MediaType.MMC; break; default: if(deviceInfo.IsPcmcia) mediaType = MediaType.PCCardTypeII; else if(deviceInfo.IsCompactFlash) mediaType = MediaType.CompactFlash; else mediaType = MediaType.GENERIC_HDD; break; } PluginBase plugins = GetPluginBase.Instance; foreach(IWritableImage plugin in plugins.WritableImages.Values.Where(p => p.SupportedMediaTypes.Contains(mediaType))) PluginsList.Add(new ImagePluginModel { Plugin = plugin }); Encodings.AddRange(Encoding.GetEncodings().Select(info => new EncodingModel { Name = info.Name, DisplayName = info.GetEncoding().EncodingName })); Encodings.AddRange(Claunia.Encoding.Encoding.GetEncodings().Select(info => new EncodingModel { Name = info.Name, DisplayName = info.DisplayName })); switch(mediaType) { case MediaType.CD: case MediaType.CDDA: case MediaType.CDG: case MediaType.CDEG: case MediaType.CDI: case MediaType.CDROM: case MediaType.CDROMXA: case MediaType.CDPLUS: case MediaType.CDMO: case MediaType.CDR: case MediaType.CDRW: case MediaType.CDMRW: case MediaType.VCD: case MediaType.SVCD: case MediaType.PCD: case MediaType.DDCD: case MediaType.DDCDR: case MediaType.DDCDRW: case MediaType.DTSCD: case MediaType.CDMIDI: case MediaType.CDV: case MediaType.CDIREADY: case MediaType.FMTOWNS: case MediaType.PS1CD: case MediaType.PS2CD: case MediaType.MEGACD: case MediaType.SATURNCD: case MediaType.GDROM: case MediaType.GDR: case MediaType.MilCD: case MediaType.SuperCDROM2: case MediaType.JaguarCD: case MediaType.ThreeDO: case MediaType.PCFX: case MediaType.NeoGeoCD: case MediaType.CDTV: case MediaType.CD32: case MediaType.Playdia: case MediaType.Pippin: case MediaType.VideoNow: case MediaType.VideoNowColor: case MediaType.VideoNowXp: case MediaType.CVD: Track1PregapVisible = true; break; default: Track1PregapVisible = false; break; } _devicePath = devicePath; } public ReactiveCommand StartCommand { get; } public ReactiveCommand CloseCommand { get; } public ReactiveCommand StopCommand { get; } public ReactiveCommand DestinationCommand { get; } public ObservableCollection PluginsList { get; } public ObservableCollection Encodings { get; } public string Title { get; } public bool OptionsVisible { get => _optionsVisible; set => this.RaiseAndSetIfChanged(ref _optionsVisible, value); } public ImagePluginModel SelectedPlugin { get => _selectedPlugin; set { this.RaiseAndSetIfChanged(ref _selectedPlugin, value); Destination = ""; if(value is null) { DestinationEnabled = false; return; } DestinationEnabled = true; if(!value.Plugin.SupportedOptions.Any()) { // Hide options } /* TODO: Plugin options grpOptions.Visible = true; var stkOptions = new StackLayout { Orientation = Orientation.Vertical }; foreach((string name, Type type, string description, object @default) option in plugin.SupportedOptions) switch(option.type.ToString()) { case "System.Boolean": var optBoolean = new CheckBox(); optBoolean.ID = "opt" + option.name; optBoolean.Text = option.description; optBoolean.Checked = (bool)option.@default; stkOptions.Items.Add(optBoolean); break; case "System.SByte": case "System.Int16": case "System.Int32": case "System.Int64": var stkNumber = new StackLayout(); stkNumber.Orientation = Orientation.Horizontal; var optNumber = new NumericStepper(); optNumber.ID = "opt" + option.name; optNumber.Value = Convert.ToDouble(option.@default); stkNumber.Items.Add(optNumber); var lblNumber = new Label(); lblNumber.Text = option.description; stkNumber.Items.Add(lblNumber); stkOptions.Items.Add(stkNumber); break; case "System.Byte": case "System.UInt16": case "System.UInt32": case "System.UInt64": var stkUnsigned = new StackLayout(); stkUnsigned.Orientation = Orientation.Horizontal; var optUnsigned = new NumericStepper(); optUnsigned.ID = "opt" + option.name; optUnsigned.MinValue = 0; optUnsigned.Value = Convert.ToDouble(option.@default); stkUnsigned.Items.Add(optUnsigned); var lblUnsigned = new Label(); lblUnsigned.Text = option.description; stkUnsigned.Items.Add(lblUnsigned); stkOptions.Items.Add(stkUnsigned); break; case "System.Single": case "System.Double": var stkFloat = new StackLayout(); stkFloat.Orientation = Orientation.Horizontal; var optFloat = new NumericStepper(); optFloat.ID = "opt" + option.name; optFloat.DecimalPlaces = 2; optFloat.Value = Convert.ToDouble(option.@default); stkFloat.Items.Add(optFloat); var lblFloat = new Label(); lblFloat.Text = option.description; stkFloat.Items.Add(lblFloat); stkOptions.Items.Add(stkFloat); break; case "System.Guid": // TODO break; case "System.String": var stkString = new StackLayout(); stkString.Orientation = Orientation.Horizontal; var lblString = new Label(); lblString.Text = option.description; stkString.Items.Add(lblString); var optString = new TextBox(); optString.ID = "opt" + option.name; optString.Text = (string)option.@default; stkString.Items.Add(optString); stkOptions.Items.Add(stkString); break; } grpOptions.Content = stkOptions; */ } } public string FormatReadOnly { get => _formatReadOnly; set => this.RaiseAndSetIfChanged(ref _formatReadOnly, value); } public string Destination { get => _destination; set => this.RaiseAndSetIfChanged(ref _destination, value); } public bool DestinationEnabled { get => _destinationEnabled; set => this.RaiseAndSetIfChanged(ref _destinationEnabled, value); } public bool StopOnError { get => _stopOnError; set => this.RaiseAndSetIfChanged(ref _stopOnError, value); } public bool Force { get => _force; set => this.RaiseAndSetIfChanged(ref _force, value); } public double Retries { get => _retries; set => this.RaiseAndSetIfChanged(ref _retries, value); } public bool Persistent { get => _persistent; set => this.RaiseAndSetIfChanged(ref _persistent, value); } public bool Resume { get => _useResume; set { this.RaiseAndSetIfChanged(ref _useResume, value); if(value == false) return; if(_outputPrefix != null) CheckResumeFile(); } } public bool Track1Pregap { get => _track1Pregap; set => this.RaiseAndSetIfChanged(ref _track1Pregap, value); } public bool Track1PregapVisible { get => _track1PregapVisible; set => this.RaiseAndSetIfChanged(ref _track1PregapVisible, value); } public double Skipped { get => _skipped; set => this.RaiseAndSetIfChanged(ref _skipped, value); } public bool Sidecar { get => _useSidecar; set { this.RaiseAndSetIfChanged(ref _useSidecar, value); EncodingVisible = value; } } public bool EncodingVisible { get => _encodingVisible; set => this.RaiseAndSetIfChanged(ref _encodingVisible, value); } public bool Trim { get => _trim; set => this.RaiseAndSetIfChanged(ref _trim, value); } public bool ExistingMetadata { get => _existingMetadata; set { this.RaiseAndSetIfChanged(ref _existingMetadata, value); if(value == false) { _sidecar = null; return; } var dlgMetadata = new OpenFileDialog { Title = "Choose existing metadata sidecar" }; dlgMetadata.Filters.Add(new FileDialogFilter { Name = "CICM XML metadata", Extensions = new List(new[] { ".xml" }) }); string[] result = dlgMetadata.ShowAsync(_view).Result; if(result?.Length != 1) { ExistingMetadata = false; return; } var sidecarXs = new XmlSerializer(typeof(CICMMetadataType)); try { var sr = new StreamReader(result[0]); _sidecar = (CICMMetadataType)sidecarXs.Deserialize(sr); sr.Close(); } catch { // ReSharper disable AssignmentIsFullyDiscarded _ = MessageBoxManager. // ReSharper restore AssignmentIsFullyDiscarded GetMessageBoxStandardWindow("Error", "Incorrect metadata sidecar file...", ButtonEnum.Ok, Icon.Error).ShowDialog(_view).Result; ExistingMetadata = false; } } } public EncodingModel SelectedEncoding { get => _selectedEncoding; set => this.RaiseAndSetIfChanged(ref _selectedEncoding, value); } public string EncodingEnabled { get => _encodingEnabled; set => this.RaiseAndSetIfChanged(ref _encodingEnabled, value); } public bool ProgressVisible { get => _progressVisible; set => this.RaiseAndSetIfChanged(ref _progressVisible, value); } public string Log { get => _log; set => this.RaiseAndSetIfChanged(ref _log, value); } public bool Progress1Visible { get => _progress1Visible; set => this.RaiseAndSetIfChanged(ref _progress1Visible, value); } public string ProgressText { get => _progressText; set => this.RaiseAndSetIfChanged(ref _progressText, value); } public double ProgressValue { get => _progressValue; set => this.RaiseAndSetIfChanged(ref _progressValue, value); } public double ProgressMaxValue { get => _progressMaxValue; set => this.RaiseAndSetIfChanged(ref _progressMaxValue, value); } public bool ProgressIndeterminate { get => _progressIndeterminate; set => this.RaiseAndSetIfChanged(ref _progressIndeterminate, value); } public bool Progress2Visible { get => _progress2Visible; set => this.RaiseAndSetIfChanged(ref _progress2Visible, value); } public string Progress2Text { get => _progress2Text; set => this.RaiseAndSetIfChanged(ref _progress2Text, value); } public double Progress2Value { get => _progress2Value; set => this.RaiseAndSetIfChanged(ref _progress2Value, value); } public double Progress2MaxValue { get => _progress2MaxValue; set => this.RaiseAndSetIfChanged(ref _progress2MaxValue, value); } public bool Progress2Indeterminate { get => _progress2Indeterminate; set => this.RaiseAndSetIfChanged(ref _progress2Indeterminate, value); } public bool StartVisible { get => _startVisible; set => this.RaiseAndSetIfChanged(ref _startVisible, value); } public bool CloseVisible { get => _closeVisible; set => this.RaiseAndSetIfChanged(ref _closeVisible, value); } public bool StopVisible { get => _stopVisible; set => this.RaiseAndSetIfChanged(ref _stopVisible, value); } public bool StopEnabled { get => _stopEnabled; set => this.RaiseAndSetIfChanged(ref _stopEnabled, value); } async Task ExecuteDestinationCommand() { if(SelectedPlugin is null) return; var dlgDestination = new SaveFileDialog { Title = "Choose destination file" }; dlgDestination.Filters.Add(new FileDialogFilter { Name = SelectedPlugin.Plugin.Name, Extensions = SelectedPlugin.Plugin.KnownExtensions.ToList() }); string result = await dlgDestination.ShowAsync(_view); if(result is null) { Destination = ""; _outputPrefix = null; return; } if(string.IsNullOrEmpty(Path.GetExtension(result))) result += SelectedPlugin.Plugin.KnownExtensions.First(); Destination = result; _outputPrefix = Path.Combine(Path.GetDirectoryName(result) ?? "", Path.GetFileNameWithoutExtension(result)); Resume = true; } async Task CheckResumeFile() { _resume = null; var xs = new XmlSerializer(typeof(Resume)); try { var sr = new StreamReader(_outputPrefix + ".resume.xml"); _resume = (Resume)xs.Deserialize(sr); sr.Close(); } catch { await MessageBoxManager. GetMessageBoxStandardWindow("Error", "Incorrect resume file, cannot use it...", ButtonEnum.Ok, Icon.Error).ShowDialog(_view); Resume = false; return; } if(_resume == null || _resume.NextBlock <= _resume.LastBlock || _resume.BadBlocks.Count != 0 && !_resume.Tape) return; await MessageBoxManager. GetMessageBoxStandardWindow("Warning", "Media already dumped correctly, please choose another destination...", ButtonEnum.Ok, Icon.Warning).ShowDialog(_view); Resume = false; } void ExecuteCloseCommand() => _view.Close(); internal void ExecuteStopCommand() { StopEnabled = false; _dumper?.Abort(); } void ExecuteStartCommand() { Log = ""; CloseVisible = false; StartVisible = false; StopVisible = true; StopEnabled = true; ProgressVisible = true; DestinationEnabled = false; OptionsVisible = false; UpdateStatus("Opening device..."); try { _dev = new Device(_devicePath); if(_dev.IsRemote) Statistics.AddRemote(_dev.RemoteApplication, _dev.RemoteVersion, _dev.RemoteOperatingSystem, _dev.RemoteOperatingSystemVersion, _dev.RemoteArchitecture); if(_dev.Error) { StoppingErrorMessage($"Error {_dev.LastError} opening device."); return; } } catch(Exception exception) { StoppingErrorMessage($"Exception {exception.Message} opening device."); return; } Statistics.AddDevice(_dev); Statistics.AddCommand("dump-media"); if(SelectedPlugin is null) { StoppingErrorMessage("Cannot open output plugin."); return; } Encoding encoding = null; if(!(SelectedEncoding is null)) try { encoding = Claunia.Encoding.Encoding.GetEncoding(SelectedEncoding.Name); } catch(ArgumentException) { StoppingErrorMessage("Specified encoding is not supported."); return; } var parsedOptions = new Dictionary(); /* TODO: Options if(grpOptions.Content is StackLayout stkFormatOptions) foreach(Control option in stkFormatOptions.Children) { string value; switch(option) { case CheckBox optBoolean: value = optBoolean.Checked?.ToString(); break; case NumericStepper optNumber: value = optNumber.Value.ToString(CultureInfo.CurrentCulture); break; case TextBox optString: value = optString.Text; break; default: continue; } string key = option.ID.Substring(3); parsedOptions.Add(key, value); } */ var dumpLog = new DumpLog(_outputPrefix + ".log", _dev, false); dumpLog.WriteLine("Output image format: {0}.", SelectedPlugin.Name); var errorLog = new ErrorLog(_outputPrefix + ".error.log"); _dumper = new Dump(Resume, _dev, _devicePath, SelectedPlugin.Plugin, (ushort)Retries, Force, false, Persistent, StopOnError, _resume, dumpLog, encoding, _outputPrefix, Destination, parsedOptions, _sidecar, (uint)Skipped, ExistingMetadata == false, Trim == false, Track1Pregap, true, false, DumpSubchannel.Any, 0, false, false, false, false, false, true, errorLog, false, 64, true, true, false); new Thread(DoWork).Start(); } async void DoWork() { _dumper.UpdateStatus += UpdateStatus; _dumper.ErrorMessage += ErrorMessage; _dumper.StoppingErrorMessage += StoppingErrorMessage; _dumper.PulseProgress += PulseProgress; _dumper.InitProgress += InitProgress; _dumper.UpdateProgress += UpdateProgress; _dumper.EndProgress += EndProgress; _dumper.InitProgress2 += InitProgress2; _dumper.UpdateProgress2 += UpdateProgress2; _dumper.EndProgress2 += EndProgress2; _dumper.Start(); _dev.Close(); await WorkFinished(); } async Task WorkFinished() => await Dispatcher.UIThread.InvokeAsync(() => { CloseVisible = true; StopVisible = false; Progress1Visible = false; Progress2Visible = false; }); async void EndProgress2() => await Dispatcher.UIThread.InvokeAsync(() => { Progress2Visible = false; }); async void UpdateProgress2(string text, long current, long maximum) => await Dispatcher.UIThread.InvokeAsync(() => { Progress2Text = text; Progress2Indeterminate = false; Progress2MaxValue = maximum; Progress2Value = current; }); async void InitProgress2() => await Dispatcher.UIThread.InvokeAsync(() => { Progress2Visible = true; }); async void EndProgress() => await Dispatcher.UIThread.InvokeAsync(() => { Progress1Visible = false; }); async void UpdateProgress(string text, long current, long maximum) => await Dispatcher.UIThread.InvokeAsync(() => { ProgressText = text; ProgressIndeterminate = false; ProgressMaxValue = maximum; ProgressValue = current; }); async void InitProgress() => await Dispatcher.UIThread.InvokeAsync(() => { Progress1Visible = true; }); async void PulseProgress(string text) => await Dispatcher.UIThread.InvokeAsync(() => { ProgressText = text; ProgressIndeterminate = true; }); async void StoppingErrorMessage(string text) => await Dispatcher.UIThread.InvokeAsync(async () => { ErrorMessage(text); await MessageBoxManager.GetMessageBoxStandardWindow("Error", $"{text}", ButtonEnum.Ok, Icon.Error). ShowDialog(_view); await WorkFinished(); }); async void ErrorMessage(string text) => await Dispatcher.UIThread.InvokeAsync(() => { Log += text + Environment.NewLine; }); async void UpdateStatus(string text) => await Dispatcher.UIThread.InvokeAsync(() => { Log += text + Environment.NewLine; }); }