mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-04-05 21:44:17 +00:00
807 lines
35 KiB
C#
807 lines
35 KiB
C#
// /***************************************************************************
|
||
// Aaru Data Preservation Suite
|
||
// ----------------------------------------------------------------------------
|
||
//
|
||
// Filename : Dump.cs
|
||
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
||
//
|
||
// Component : Commands.
|
||
//
|
||
// --[ Description ] ----------------------------------------------------------
|
||
//
|
||
// Implements the 'dump' command.
|
||
//
|
||
// --[ 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 <http://www.gnu.org/licenses/>.
|
||
//
|
||
// ----------------------------------------------------------------------------
|
||
// Copyright © 2011-2026 Natalia Portillo
|
||
// Copyright © 2020-2026 Rebecca Wallander
|
||
// ****************************************************************************/
|
||
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.ComponentModel;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
using System.Threading;
|
||
using System.Xml.Serialization;
|
||
using Aaru.CommonTypes;
|
||
using Aaru.CommonTypes.AaruMetadata;
|
||
using Aaru.CommonTypes.Enums;
|
||
using Aaru.CommonTypes.Interfaces;
|
||
using Aaru.CommonTypes.Interop;
|
||
using Aaru.CommonTypes.Metadata;
|
||
using Aaru.CommonTypes.Structs.Devices.SCSI;
|
||
using Aaru.Core;
|
||
using Aaru.Core.Devices.Dumping;
|
||
using Aaru.Core.Logging;
|
||
using Aaru.Localization;
|
||
using Aaru.Logging;
|
||
using Schemas;
|
||
using Sentry;
|
||
using Spectre.Console;
|
||
using Spectre.Console.Cli;
|
||
using Dump = Aaru.Core.Devices.Dumping.Dump;
|
||
using File = System.IO.File;
|
||
|
||
namespace Aaru.Commands.Media;
|
||
|
||
// TODO: Add raw dumping
|
||
sealed class DumpMediaCommand : Command<DumpMediaCommand.Settings>
|
||
{
|
||
const string MODULE_NAME = "Dump-Media command";
|
||
static ProgressTask _progressTask1;
|
||
static ProgressTask _progressTask2;
|
||
|
||
public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken)
|
||
{
|
||
MainClass.PrintCopyright();
|
||
|
||
bool fixSubchannel = settings.FixSubchannel;
|
||
bool fixSubchannelCrc = settings.FixSubchannelCrc;
|
||
bool fixSubchannelPosition = settings.FixSubchannelPosition;
|
||
var maxBlocks = (uint)settings.MaxBlocks;
|
||
bool eject = settings.Eject;
|
||
|
||
fixSubchannel |= fixSubchannelCrc;
|
||
fixSubchannelPosition |= settings.RetrySubchannel || fixSubchannel;
|
||
|
||
if(maxBlocks == 0) maxBlocks = 64;
|
||
|
||
Statistics.AddCommand("dump-media");
|
||
|
||
AaruLogging.Debug(MODULE_NAME, "--cicm-xml={0}", Markup.Escape(settings.CicmXml ?? ""));
|
||
AaruLogging.Debug(MODULE_NAME, "--debug={0}", settings.Debug);
|
||
AaruLogging.Debug(MODULE_NAME, "--device={0}", Markup.Escape(settings.DevicePath ?? ""));
|
||
AaruLogging.Debug(MODULE_NAME, "--encoding={0}", Markup.Escape(settings.Encoding ?? ""));
|
||
AaruLogging.Debug(MODULE_NAME, "--first-pregap={0}", settings.FirstPregap);
|
||
AaruLogging.Debug(MODULE_NAME, "--fix-offset={0}", settings.FixOffset);
|
||
AaruLogging.Debug(MODULE_NAME, "--force={0}", settings.Force);
|
||
AaruLogging.Debug(MODULE_NAME, "--format={0}", Markup.Escape(settings.Format ?? ""));
|
||
AaruLogging.Debug(MODULE_NAME, "--metadata={0}", settings.Metadata);
|
||
AaruLogging.Debug(MODULE_NAME, "--options={0}", Markup.Escape(settings.Options ?? ""));
|
||
AaruLogging.Debug(MODULE_NAME, "--output={0}", Markup.Escape(settings.OutputPath ?? ""));
|
||
AaruLogging.Debug(MODULE_NAME, "--persistent={0}", settings.Persistent);
|
||
AaruLogging.Debug(MODULE_NAME, "--resume={0}", settings.Resume);
|
||
AaruLogging.Debug(MODULE_NAME, "--retry-passes={0}", settings.RetryPasses);
|
||
AaruLogging.Debug(MODULE_NAME, "--skip={0}", settings.Skip);
|
||
AaruLogging.Debug(MODULE_NAME, "--stop-on-error={0}", settings.StopOnError);
|
||
AaruLogging.Debug(MODULE_NAME, "--trim={0}", settings.Trim);
|
||
AaruLogging.Debug(MODULE_NAME, "--verbose={0}", settings.Verbose);
|
||
AaruLogging.Debug(MODULE_NAME, "--subchannel={0}", Markup.Escape(settings.Subchannel ?? ""));
|
||
AaruLogging.Debug(MODULE_NAME, "----private={0}", settings.Private);
|
||
AaruLogging.Debug(MODULE_NAME, "--fix-subchannel-position={0}", settings.FixSubchannelPosition);
|
||
AaruLogging.Debug(MODULE_NAME, "--retry-subchannel={0}", settings.RetrySubchannel);
|
||
AaruLogging.Debug(MODULE_NAME, "--fix-subchannel={0}", fixSubchannel);
|
||
AaruLogging.Debug(MODULE_NAME, "--fix-subchannel-crc={0}", fixSubchannelCrc);
|
||
AaruLogging.Debug(MODULE_NAME, "--generate-subchannels={0}", settings.GenerateSubchannels);
|
||
AaruLogging.Debug(MODULE_NAME, "--skip-cdiready-hole={0}", settings.SkipCdiReadyHole);
|
||
AaruLogging.Debug(MODULE_NAME, "--eject={0}", eject);
|
||
AaruLogging.Debug(MODULE_NAME, "--max-blocks={0}", maxBlocks);
|
||
AaruLogging.Debug(MODULE_NAME, "--use-buffered-reads={0}", settings.UseBufferedReads);
|
||
AaruLogging.Debug(MODULE_NAME, "--store-encrypted={0}", settings.StoreEncrypted);
|
||
AaruLogging.Debug(MODULE_NAME, "--bypass-wii-decryption={0}", settings.BypassWiiDecryption);
|
||
AaruLogging.Debug(MODULE_NAME, "--title-keys={0}", settings.TitleKeys);
|
||
AaruLogging.Debug(MODULE_NAME, "--ignore-cdr-runouts={0}", settings.IgnoreCdrRunOuts);
|
||
AaruLogging.Debug(MODULE_NAME, "--create-graph={0}", settings.CreateGraph);
|
||
AaruLogging.Debug(MODULE_NAME, "--dimensions={0}", settings.Dimensions);
|
||
AaruLogging.Debug(MODULE_NAME, "--aaru-metadata={0}", Markup.Escape(settings.AaruMetadata ?? ""));
|
||
AaruLogging.Debug(MODULE_NAME, "--paranoia={0}", settings.Paranoia);
|
||
AaruLogging.Debug(MODULE_NAME, "--cure-paranoia={0}", settings.CureParanoia);
|
||
AaruLogging.Debug(MODULE_NAME, "--raw={0}", settings.Raw);
|
||
|
||
Dictionary<string, string> parsedOptions = Options.Parse(settings.Options);
|
||
AaruLogging.Debug(MODULE_NAME, UI.Parsed_options);
|
||
|
||
foreach(KeyValuePair<string, string> parsedOption in parsedOptions)
|
||
AaruLogging.Debug(MODULE_NAME, "{0} = {1}", parsedOption.Key, parsedOption.Value);
|
||
|
||
Encoding encodingClass = null;
|
||
|
||
if(settings.Encoding != null)
|
||
{
|
||
try
|
||
{
|
||
encodingClass = Claunia.Encoding.Encoding.GetEncoding(settings.Encoding);
|
||
|
||
if(settings.Verbose) AaruLogging.Verbose(UI.encoding_for_0, encodingClass.EncodingName);
|
||
}
|
||
catch(ArgumentException)
|
||
{
|
||
AaruLogging.Error(UI.Specified_encoding_is_not_supported);
|
||
|
||
return (int)ErrorNumber.EncodingUnknown;
|
||
}
|
||
}
|
||
|
||
DumpSubchannel wantedSubchannel = DumpSubchannel.Any;
|
||
|
||
if(settings.Subchannel?.ToLower(CultureInfo.CurrentUICulture) == UI.Subchannel_name_any ||
|
||
settings.Subchannel is null)
|
||
wantedSubchannel = DumpSubchannel.Any;
|
||
else if(settings.Subchannel?.ToLowerInvariant() == UI.Subchannel_name_rw)
|
||
wantedSubchannel = DumpSubchannel.Rw;
|
||
else if(settings.Subchannel?.ToLowerInvariant() == UI.Subchannel_name_rw_or_pq)
|
||
wantedSubchannel = DumpSubchannel.RwOrPq;
|
||
else if(settings.Subchannel?.ToLowerInvariant() == UI.Subchannel_name_pq)
|
||
wantedSubchannel = DumpSubchannel.Pq;
|
||
else if(settings.Subchannel?.ToLowerInvariant() == UI.Subchannel_name_none)
|
||
wantedSubchannel = DumpSubchannel.None;
|
||
else
|
||
AaruLogging.WriteLine(UI.Incorrect_subchannel_type_0_requested, settings.Subchannel);
|
||
|
||
string filename = Path.GetFileNameWithoutExtension(settings.OutputPath);
|
||
|
||
bool isResponse = filename.StartsWith("#", StringComparison.OrdinalIgnoreCase) &&
|
||
File.Exists(Path.Combine(Path.GetDirectoryName(settings.OutputPath),
|
||
Path.GetFileNameWithoutExtension(settings.OutputPath)));
|
||
|
||
TextReader resReader;
|
||
|
||
if(isResponse)
|
||
{
|
||
resReader = new StreamReader(Path.Combine(Path.GetDirectoryName(settings.OutputPath),
|
||
Path.GetFileNameWithoutExtension(settings.OutputPath)));
|
||
}
|
||
else
|
||
resReader = new StringReader(Path.GetFileNameWithoutExtension(settings.OutputPath));
|
||
|
||
if(isResponse) eject = true;
|
||
|
||
PluginRegister plugins = PluginRegister.Singleton;
|
||
List<IBaseWritableImage> candidates = [];
|
||
string extension = Path.GetExtension(settings.OutputPath);
|
||
|
||
// Try extension
|
||
if(string.IsNullOrEmpty(settings.Format))
|
||
{
|
||
candidates.AddRange(from plugin in plugins.WritableImages.Values
|
||
where plugin is not null
|
||
where plugin.KnownExtensions.Contains(extension)
|
||
select plugin);
|
||
}
|
||
|
||
// Try Id
|
||
else if(Guid.TryParse(settings.Format, out Guid outId))
|
||
{
|
||
candidates.AddRange(from plugin in plugins.WritableImages.Values
|
||
where plugin is not null
|
||
where plugin.Id.Equals(outId)
|
||
select plugin);
|
||
}
|
||
|
||
// Try name
|
||
else
|
||
{
|
||
candidates.AddRange(from plugin in plugins.WritableImages.Values
|
||
where plugin is not null
|
||
where plugin.Name.Equals(settings.Format, StringComparison.InvariantCultureIgnoreCase)
|
||
select plugin);
|
||
}
|
||
|
||
switch(candidates.Count)
|
||
{
|
||
case 0:
|
||
AaruLogging.Error(UI.No_plugin_supports_requested_extension);
|
||
|
||
return (int)ErrorNumber.FormatNotFound;
|
||
case > 1:
|
||
AaruLogging.Error(UI.More_than_one_plugin_supports_requested_extension);
|
||
|
||
return (int)ErrorNumber.TooManyFormats;
|
||
}
|
||
|
||
while(true)
|
||
{
|
||
string responseLine = resReader.ReadLine();
|
||
|
||
if(responseLine is null) break;
|
||
|
||
if(responseLine.Any(static c => c < 0x20))
|
||
{
|
||
AaruLogging.Error(UI.Invalid_characters_found_in_list_of_files);
|
||
|
||
return (int)ErrorNumber.InvalidArgument;
|
||
}
|
||
|
||
if(isResponse)
|
||
{
|
||
AaruLogging.WriteLine(UI.Please_insert_media_with_title_0_and_press_any_key_to_continue_, responseLine);
|
||
|
||
Console.ReadKey();
|
||
Thread.Sleep(1000);
|
||
}
|
||
|
||
responseLine = responseLine.Replace('/', '/');
|
||
|
||
// Replace Windows forbidden filename characters with Japanese equivalents that are visually the same, but bigger.
|
||
if(DetectOS.IsWindows)
|
||
{
|
||
responseLine = responseLine.Replace('<', '\uFF1C')
|
||
.Replace('>', '\uFF1E')
|
||
.Replace(':', '\uFF1A')
|
||
.Replace('"', '\u2033')
|
||
.Replace('\\', '\')
|
||
.Replace('|', '|')
|
||
.Replace('?', '?')
|
||
.Replace('*', '*');
|
||
}
|
||
|
||
string devicePath = settings.DevicePath;
|
||
|
||
if(devicePath.Length == 2 && devicePath[1] == ':' && devicePath[0] != '/' && char.IsLetter(devicePath[0]))
|
||
devicePath = "\\\\.\\" + char.ToUpper(devicePath[0]) + ':';
|
||
|
||
Devices.Device dev = null;
|
||
ErrorNumber devErrno = ErrorNumber.NoError;
|
||
|
||
Core.Spectre.ProgressSingleSpinner(ctx =>
|
||
{
|
||
ctx.AddTask(UI.Opening_device).IsIndeterminate();
|
||
dev = Devices.Device.Create(devicePath, out devErrno);
|
||
});
|
||
|
||
switch(dev)
|
||
{
|
||
case null:
|
||
{
|
||
AaruLogging.Error(string.Format(UI.Could_not_open_device_error_0, devErrno));
|
||
|
||
if(isResponse) continue;
|
||
|
||
return (int)devErrno;
|
||
}
|
||
case Devices.Remote.Device remoteDev:
|
||
Statistics.AddRemote(remoteDev.RemoteApplication,
|
||
remoteDev.RemoteVersion,
|
||
remoteDev.RemoteOperatingSystem,
|
||
remoteDev.RemoteOperatingSystemVersion,
|
||
remoteDev.RemoteArchitecture);
|
||
|
||
break;
|
||
}
|
||
|
||
if(dev.Error)
|
||
{
|
||
AaruLogging.Error(Error.Print(dev.LastError));
|
||
|
||
if(isResponse) continue;
|
||
|
||
return (int)ErrorNumber.CannotOpenDevice;
|
||
}
|
||
|
||
Statistics.AddDevice(dev);
|
||
|
||
string outputPrefix = Path.Combine(Path.GetDirectoryName(settings.OutputPath), responseLine);
|
||
|
||
Resume resumeClass = null;
|
||
|
||
if(settings.Resume)
|
||
{
|
||
try
|
||
{
|
||
if(File.Exists(outputPrefix + ".resume.json"))
|
||
{
|
||
var fs = new FileStream(outputPrefix + ".resume.json", FileMode.Open);
|
||
|
||
resumeClass =
|
||
(JsonSerializer.Deserialize(fs,
|
||
typeof(ResumeJson),
|
||
ResumeJsonContext.Default) as ResumeJson)?.Resume;
|
||
|
||
fs.Close();
|
||
}
|
||
|
||
// DEPRECATED: To be removed in Aaru 7
|
||
else if(File.Exists(outputPrefix + ".resume.xml") && settings.Resume)
|
||
{
|
||
// Should be covered by virtue of being the same exact class as the JSON above
|
||
#pragma warning disable IL2026
|
||
var xs = new XmlSerializer(typeof(Resume));
|
||
#pragma warning restore IL2026
|
||
|
||
var sr = new StreamReader(outputPrefix + ".resume.xml");
|
||
|
||
// Should be covered by virtue of being the same exact class as the JSON above
|
||
#pragma warning disable IL2026
|
||
resumeClass = (Resume)xs.Deserialize(sr);
|
||
#pragma warning restore IL2026
|
||
|
||
sr.Close();
|
||
}
|
||
}
|
||
catch(Exception ex)
|
||
{
|
||
SentrySdk.CaptureException(ex);
|
||
AaruLogging.Error(UI.Incorrect_resume_file_not_continuing);
|
||
|
||
if(isResponse) continue;
|
||
|
||
return (int)ErrorNumber.InvalidResume;
|
||
}
|
||
}
|
||
|
||
if(resumeClass != null &&
|
||
resumeClass.NextBlock > resumeClass.LastBlock &&
|
||
resumeClass.BadBlocks.Count == 0 &&
|
||
!resumeClass.Tape &&
|
||
(resumeClass.BadSubchannels is null || resumeClass.BadSubchannels.Count == 0) &&
|
||
(resumeClass.MissingTitleKeys is null || resumeClass.MissingTitleKeys.Count == 0))
|
||
{
|
||
AaruLogging.WriteLine(UI.Media_already_dumped_correctly_not_continuing);
|
||
|
||
if(isResponse) continue;
|
||
|
||
return (int)ErrorNumber.AlreadyDumped;
|
||
}
|
||
|
||
Metadata sidecar = null;
|
||
|
||
if(settings.AaruMetadata != null)
|
||
{
|
||
if(File.Exists(settings.AaruMetadata))
|
||
{
|
||
try
|
||
{
|
||
var fs = new FileStream(settings.AaruMetadata, FileMode.Open);
|
||
|
||
sidecar =
|
||
(JsonSerializer.Deserialize(fs, typeof(MetadataJson), MetadataJsonContext.Default) as
|
||
MetadataJson)?.AaruMetadata;
|
||
|
||
fs.Close();
|
||
}
|
||
catch(Exception ex)
|
||
{
|
||
SentrySdk.CaptureException(ex);
|
||
AaruLogging.Error(UI.Incorrect_metadata_sidecar_file_not_continuing);
|
||
|
||
if(isResponse) continue;
|
||
|
||
return (int)ErrorNumber.InvalidSidecar;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
AaruLogging.Error(UI.Could_not_find_metadata_sidecar);
|
||
|
||
if(isResponse) continue;
|
||
|
||
return (int)ErrorNumber.NoSuchFile;
|
||
}
|
||
}
|
||
else if(settings.CicmXml != null)
|
||
{
|
||
if(File.Exists(settings.CicmXml))
|
||
{
|
||
try
|
||
{
|
||
var sr = new StreamReader(settings.CicmXml);
|
||
|
||
// Bypassed by JSON source generator used above
|
||
#pragma warning disable IL2026
|
||
var sidecarXs = new XmlSerializer(typeof(CICMMetadataType));
|
||
#pragma warning restore IL2026
|
||
|
||
// Bypassed by JSON source generator used above
|
||
#pragma warning disable IL2026
|
||
sidecar = (CICMMetadataType)sidecarXs.Deserialize(sr);
|
||
#pragma warning restore IL2026
|
||
|
||
sr.Close();
|
||
}
|
||
catch(Exception ex)
|
||
{
|
||
SentrySdk.CaptureException(ex);
|
||
AaruLogging.Error(UI.Incorrect_metadata_sidecar_file_not_continuing);
|
||
|
||
if(isResponse) continue;
|
||
|
||
return (int)ErrorNumber.InvalidSidecar;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
AaruLogging.Error(UI.Could_not_find_metadata_sidecar);
|
||
|
||
if(isResponse) continue;
|
||
|
||
return (int)ErrorNumber.NoSuchFile;
|
||
}
|
||
}
|
||
|
||
plugins = PluginRegister.Singleton;
|
||
candidates = [];
|
||
|
||
// Try extension
|
||
if(string.IsNullOrEmpty(settings.Format))
|
||
{
|
||
candidates.AddRange(from plugin in plugins.WritableImages.Values
|
||
where plugin is not null
|
||
where plugin.KnownExtensions.Contains(Path.GetExtension(settings.OutputPath))
|
||
select plugin);
|
||
}
|
||
|
||
// Try Id
|
||
else if(Guid.TryParse(settings.Format, out Guid outId))
|
||
{
|
||
candidates.AddRange(from plugin in plugins.WritableImages.Values
|
||
where plugin is not null
|
||
where plugin.Id.Equals(outId)
|
||
select plugin);
|
||
}
|
||
|
||
// Try name
|
||
else
|
||
{
|
||
candidates.AddRange(from plugin in plugins.WritableImages.Values
|
||
where plugin is not null
|
||
where plugin.Name.Equals(settings.Format,
|
||
StringComparison.InvariantCultureIgnoreCase)
|
||
select plugin);
|
||
}
|
||
|
||
IBaseWritableImage outputFormat = candidates[0];
|
||
|
||
DeviceLog.StartLog(dev, settings.Private);
|
||
|
||
if(settings.Verbose)
|
||
AaruLogging.Verbose(UI.Output_image_format_0_1, outputFormat.Name, outputFormat.Id);
|
||
else
|
||
AaruLogging.WriteLine(UI.Output_image_format_0, outputFormat.Name);
|
||
|
||
var errorLog = new ErrorLog(outputPrefix + ".error.log");
|
||
|
||
var dumper = new Dump(settings.Resume,
|
||
dev,
|
||
devicePath,
|
||
outputFormat,
|
||
(ushort)settings.RetryPasses,
|
||
settings.Force,
|
||
settings.Raw,
|
||
settings.Persistent,
|
||
settings.StopOnError,
|
||
resumeClass,
|
||
encodingClass,
|
||
outputPrefix,
|
||
outputPrefix + extension,
|
||
parsedOptions,
|
||
sidecar,
|
||
(uint)settings.Skip,
|
||
settings.Metadata,
|
||
settings.Trim,
|
||
settings.FirstPregap,
|
||
settings.FixOffset,
|
||
settings.Debug,
|
||
wantedSubchannel,
|
||
settings.Speed,
|
||
settings.Private,
|
||
fixSubchannelPosition,
|
||
settings.RetrySubchannel,
|
||
fixSubchannel,
|
||
fixSubchannelCrc,
|
||
settings.SkipCdiReadyHole,
|
||
errorLog,
|
||
settings.GenerateSubchannels,
|
||
maxBlocks,
|
||
settings.UseBufferedReads,
|
||
settings.StoreEncrypted,
|
||
settings.TitleKeys,
|
||
(uint)settings.IgnoreCdrRunOuts,
|
||
settings.CreateGraph,
|
||
(uint)settings.Dimensions,
|
||
settings.Paranoia,
|
||
settings.CureParanoia,
|
||
settings.BypassWiiDecryption);
|
||
|
||
AnsiConsole.Progress()
|
||
.AutoClear(true)
|
||
.HideCompleted(true)
|
||
.Columns(new ProgressBarColumn(),
|
||
new RemainingTimeColumn(),
|
||
new PercentageColumn(),
|
||
new TaskDescriptionColumn())
|
||
.Start(ctx =>
|
||
{
|
||
dumper.UpdateStatus += static text => { AaruLogging.WriteLine(text); };
|
||
|
||
dumper.ErrorMessage += static text => AaruLogging.Error(text);
|
||
|
||
dumper.StoppingErrorMessage += static text => { AaruLogging.Error(text); };
|
||
|
||
dumper.UpdateProgress += (text, current, maximum) =>
|
||
{
|
||
_progressTask1 ??= ctx.AddTask("Progress");
|
||
_progressTask1.Description = text;
|
||
_progressTask1.Value = current;
|
||
_progressTask1.MaxValue = maximum;
|
||
};
|
||
|
||
dumper.PulseProgress += text =>
|
||
{
|
||
if(_progressTask1 is null)
|
||
ctx.AddTask(text).IsIndeterminate();
|
||
else
|
||
{
|
||
_progressTask1.Description = text;
|
||
_progressTask1.IsIndeterminate = true;
|
||
}
|
||
};
|
||
|
||
dumper.InitProgress += () => { _progressTask1 = ctx.AddTask("Progress"); };
|
||
|
||
dumper.EndProgress += static () =>
|
||
{
|
||
_progressTask1?.StopTask();
|
||
_progressTask1 = null;
|
||
};
|
||
|
||
dumper.InitProgress2 += () => { _progressTask2 = ctx.AddTask("Progress"); };
|
||
|
||
dumper.EndProgress2 += static () =>
|
||
{
|
||
_progressTask2?.StopTask();
|
||
_progressTask2 = null;
|
||
};
|
||
|
||
dumper.UpdateProgress2 += (text, current, maximum) =>
|
||
{
|
||
_progressTask2 ??= ctx.AddTask("Progress");
|
||
_progressTask2.Description = text;
|
||
_progressTask2.Value = current;
|
||
_progressTask2.MaxValue = maximum;
|
||
};
|
||
|
||
Console.CancelKeyPress += (_, e) =>
|
||
{
|
||
e.Cancel = true;
|
||
dumper.Abort();
|
||
};
|
||
|
||
dumper.Start();
|
||
});
|
||
|
||
if(eject && dev.IsRemovable)
|
||
{
|
||
Core.Spectre.ProgressSingleSpinner(ctx =>
|
||
{
|
||
ctx.AddTask(UI.Ejecting_media).IsIndeterminate();
|
||
|
||
switch(dev.Type)
|
||
{
|
||
case DeviceType.ATA:
|
||
dev.DoorUnlock(out _, dev.Timeout, out _);
|
||
dev.MediaEject(out _, dev.Timeout, out _);
|
||
|
||
break;
|
||
case DeviceType.ATAPI:
|
||
case DeviceType.SCSI:
|
||
switch(dev.ScsiType)
|
||
{
|
||
case PeripheralDeviceTypes.DirectAccess:
|
||
case PeripheralDeviceTypes.SimplifiedDevice:
|
||
case PeripheralDeviceTypes.SCSIZonedBlockDevice:
|
||
case PeripheralDeviceTypes.WriteOnceDevice:
|
||
case PeripheralDeviceTypes.OpticalDevice:
|
||
case PeripheralDeviceTypes.OCRWDevice:
|
||
dev.SpcAllowMediumRemoval(out _, dev.Timeout, out _);
|
||
dev.EjectTray(out _, dev.Timeout, out _);
|
||
|
||
break;
|
||
case PeripheralDeviceTypes.MultiMediaDevice:
|
||
dev.AllowMediumRemoval(out _, dev.Timeout, out _);
|
||
dev.EjectTray(out _, dev.Timeout, out _);
|
||
|
||
break;
|
||
case PeripheralDeviceTypes.SequentialAccess:
|
||
dev.SpcAllowMediumRemoval(out _, dev.Timeout, out _);
|
||
dev.LoadUnload(out _, true, false, false, false, false, dev.Timeout, out _);
|
||
|
||
break;
|
||
}
|
||
|
||
break;
|
||
}
|
||
});
|
||
}
|
||
|
||
dev.Close();
|
||
}
|
||
|
||
return (int)ErrorNumber.NoError;
|
||
}
|
||
|
||
#region Nested type: Settings
|
||
|
||
public class Settings : MediaFamily
|
||
{
|
||
[LocalizedDescription(nameof(UI.Take_metadata_from_existing_CICM_XML_sidecar))]
|
||
[CommandOption("-x|--cicm-xml")]
|
||
[DefaultValue(null)]
|
||
public string CicmXml { get; init; }
|
||
[LocalizedDescription(nameof(UI.Name_of_character_encoding_to_use))]
|
||
[CommandOption("-e|--encoding")]
|
||
[DefaultValue(null)]
|
||
public string Encoding { get; init; }
|
||
[LocalizedDescription(nameof(UI.Try_to_read_first_track_pregap))]
|
||
[CommandOption("--first-pregap")]
|
||
[DefaultValue(false)]
|
||
public bool FirstPregap { get; init; }
|
||
[LocalizedDescription(nameof(UI.Fix_audio_tracks_offset))]
|
||
[CommandOption("--fix-offset")]
|
||
[DefaultValue(true)]
|
||
public bool FixOffset { get; init; }
|
||
[LocalizedDescription(nameof(UI.Continue_dumping_whatever_happens))]
|
||
[CommandOption("-f|--force")]
|
||
[DefaultValue(false)]
|
||
public bool Force { get; init; }
|
||
[LocalizedDescription(nameof(UI.Format_of_the_output_image_as_plugin_name_or_plugin_id))]
|
||
[CommandOption("-t|--format")]
|
||
[DefaultValue(null)]
|
||
public string Format { get; init; }
|
||
[LocalizedDescription(nameof(UI.Enables_creating_Aaru_Metadata_sidecar))]
|
||
[CommandOption("--metadata")]
|
||
[DefaultValue(true)]
|
||
public bool Metadata { get; init; }
|
||
[LocalizedDescription(nameof(UI.Enables_trimming_errored_from_skipped_sectors))]
|
||
[CommandOption("--trim")]
|
||
[DefaultValue(true)]
|
||
public bool Trim { get; init; }
|
||
[LocalizedDescription(nameof(UI.Comma_separated_name_value_pairs_of_image_options))]
|
||
[CommandOption("-O|--options")]
|
||
[DefaultValue(null)]
|
||
public string Options { get; init; }
|
||
[LocalizedDescription(nameof(UI.Try_to_recover_partial_or_incorrect_data))]
|
||
[CommandOption("--persistent")]
|
||
[DefaultValue(false)]
|
||
public bool Persistent { get; init; }
|
||
[LocalizedDescription(nameof(UI.Create_or_use_resume_mapfile))]
|
||
[CommandOption("-r|--resume")]
|
||
[DefaultValue(true)]
|
||
public bool Resume { get; init; }
|
||
[LocalizedDescription(nameof(UI.How_many_retry_passes_to_do))]
|
||
[CommandOption("-p|--retry-passes")]
|
||
[DefaultValue(5)]
|
||
public int RetryPasses { get; init; }
|
||
[LocalizedDescription(nameof(UI.When_an_unreadable_sector_is_found_skip_this_many_sectors))]
|
||
[CommandOption("-k|--skip")]
|
||
[DefaultValue(512)]
|
||
public int Skip { get; init; }
|
||
[LocalizedDescription(nameof(UI.Stop_media_dump_on_first_error))]
|
||
[CommandOption("-s|--stop-on-error")]
|
||
[DefaultValue(false)]
|
||
public bool StopOnError { get; init; }
|
||
[LocalizedDescription(nameof(UI.Subchannel_to_dump_help))]
|
||
[CommandOption("--subchannel")]
|
||
[DefaultValue("any")]
|
||
public string Subchannel { get; init; }
|
||
[LocalizedDescription(nameof(UI.Speed_to_dump))]
|
||
[CommandOption("--speed")]
|
||
[DefaultValue(0)]
|
||
public int Speed { get; init; }
|
||
[LocalizedDescription(nameof(UI.Do_not_store_paths_and_serial_numbers_in_log_or_metadata))]
|
||
[CommandOption("--private")]
|
||
[DefaultValue(false)]
|
||
public bool Private { get; init; }
|
||
[LocalizedDescription(nameof(UI.Fix_subchannel_position_help))]
|
||
[CommandOption("--fix-subchannel-position")]
|
||
[DefaultValue(true)]
|
||
public bool FixSubchannelPosition { get; init; }
|
||
[LocalizedDescription(nameof(UI.Retry_subchannel_help))]
|
||
[CommandOption("--retry-subchannel")]
|
||
[DefaultValue(true)]
|
||
public bool RetrySubchannel { get; init; }
|
||
[LocalizedDescription(nameof(UI.Fix_subchannel_help))]
|
||
[CommandOption("--fix-subchannel")]
|
||
[DefaultValue(false)]
|
||
public bool FixSubchannel { get; init; }
|
||
[LocalizedDescription(nameof(UI.Fix_subchannel_crc_help))]
|
||
[CommandOption("--fix-subchannel-crc")]
|
||
[DefaultValue(false)]
|
||
public bool FixSubchannelCrc { get; init; }
|
||
[LocalizedDescription(nameof(UI.Generate_subchannels_dump_help))]
|
||
[CommandOption("--generate-subchannels")]
|
||
[DefaultValue(false)]
|
||
public bool GenerateSubchannels { get; init; }
|
||
[LocalizedDescription(nameof(UI.Skip_CDi_Ready_hole_help))]
|
||
[CommandOption("--skip-cdiready-hole")]
|
||
[DefaultValue(true)]
|
||
public bool SkipCdiReadyHole { get; init; }
|
||
[LocalizedDescription(nameof(UI.Eject_media_after_dump_finishes))]
|
||
[CommandOption("--eject")]
|
||
[DefaultValue(false)]
|
||
public bool Eject { get; init; }
|
||
[LocalizedDescription(nameof(UI.Maximum_number_of_blocks_to_read_at_once))]
|
||
[CommandOption("--max-blocks")]
|
||
[DefaultValue(64)]
|
||
public int MaxBlocks { get; init; }
|
||
[LocalizedDescription(nameof(UI.OS_buffered_reads_help))]
|
||
[CommandOption("--use-buffered-reads")]
|
||
[DefaultValue(true)]
|
||
public bool UseBufferedReads { get; init; }
|
||
[LocalizedDescription(nameof(UI.Store_encrypted_data_as_is))]
|
||
[CommandOption("--store-encrypted")]
|
||
[DefaultValue(true)]
|
||
public bool StoreEncrypted { get; init; }
|
||
[LocalizedDescription(nameof(UI.Bypass_Wii_decryption_help))]
|
||
[CommandOption("--bypass-wii-decryption")]
|
||
[DefaultValue(false)]
|
||
public bool BypassWiiDecryption { get; init; }
|
||
[LocalizedDescription(nameof(UI.Try_to_read_the_title_keys_from_CSS_DVDs))]
|
||
[CommandOption("--title-keys")]
|
||
[DefaultValue(true)]
|
||
public bool TitleKeys { get; init; }
|
||
[LocalizedDescription(nameof(UI.How_many_CDRW_run_out_sectors_to_ignore_and_regenerate))]
|
||
[CommandOption("--ignore-cdr-runouts")]
|
||
[DefaultValue(10)]
|
||
public int IgnoreCdrRunOuts { get; init; }
|
||
[LocalizedDescription(nameof(UI.Create_graph_of_dumped_media))]
|
||
[CommandOption("-g|--create-graph")]
|
||
[DefaultValue(true)]
|
||
public bool CreateGraph { get; init; }
|
||
[LocalizedDescription(nameof(UI.Dump_graph_dimensions_argument_help))]
|
||
[CommandOption("--dimensions")]
|
||
[DefaultValue(1080)]
|
||
public int Dimensions { get; init; }
|
||
[LocalizedDescription(nameof(UI.Take_metadata_from_existing_Aaru_sidecar))]
|
||
[CommandOption("--aaru-metadata")]
|
||
[DefaultValue(null)]
|
||
public string AaruMetadata { get; init; }
|
||
[LocalizedDescription(nameof(UI.Paranoia_help))]
|
||
[CommandOption("--paranoia")]
|
||
[DefaultValue(false)]
|
||
public bool Paranoia { get; init; }
|
||
[LocalizedDescription(nameof(UI.Cure_paranoia_help))]
|
||
[CommandOption("--cure-paranoia")]
|
||
[DefaultValue(false)]
|
||
public bool CureParanoia { get; init; }
|
||
[LocalizedDescription(nameof(UI.Raw_dumping_experimental_help))]
|
||
[CommandOption("--raw")]
|
||
[DefaultValue(false)]
|
||
public bool Raw { get; init; }
|
||
[LocalizedDescription(nameof(UI.Device_path))]
|
||
[CommandArgument(0, "<device-path>")]
|
||
public string DevicePath { get; init; }
|
||
[LocalizedDescription(nameof(UI.Output_image_path_Dump_help))]
|
||
[CommandArgument(1, "<output-path>")]
|
||
public string OutputPath { get; init; }
|
||
}
|
||
|
||
#endregion
|
||
} |