feat: Enhance Face AI upload functionality and UI
- Updated MainWindow.axaml to increase height and add new UI elements for SSH upload configuration. - Implemented commands for opening source and destination paths in file explorer. - Added FaceUploadPath and SSH configuration properties to DataModel and AiSettingsViewModel. - Introduced validation for FaceUploadPath format and commands for uploading face encoder output. - Enhanced PickerPreferenceService to manage SSH credentials and upload preferences. - Updated settings persistence to include FaceUploadPath and SSH preferences. - Added tests for FaceUploadPath validation and upload command enabling logic.
This commit is contained in:
parent
e68608312a
commit
e9142df97c
22 changed files with 1477 additions and 84 deletions
|
|
@ -41,6 +41,7 @@ namespace ImageCatalog_2
|
|||
public ICommand StartAiCommand { get; }
|
||||
public ICommand StartFaceEncoderCommand { get; }
|
||||
public ICommand StopFaceEncoderCommand { get; }
|
||||
public ICommand UploadFaceEncoderOutputCommand { get; }
|
||||
public ICommand StartFaceMatcherCommand { get; }
|
||||
public ICommand StopFaceMatcherCommand { get; }
|
||||
|
||||
|
|
@ -50,6 +51,7 @@ namespace ImageCatalog_2
|
|||
private readonly ImageCreationService _imageCreationService;
|
||||
private readonly IAiExtractionService _aiExtractionService;
|
||||
private readonly IImageProcessingCoordinator _imageProcessingCoordinator;
|
||||
private readonly PickerPreferenceService? _pickerPreferenceService;
|
||||
private readonly ProcessingStateViewModel _processing;
|
||||
private readonly PathSettingsViewModel _paths;
|
||||
private readonly AiSettingsViewModel _ai;
|
||||
|
|
@ -59,6 +61,7 @@ namespace ImageCatalog_2
|
|||
private readonly IMapper _mapper;
|
||||
private readonly AsyncCommand _startFaceEncoderCommand;
|
||||
private readonly AsyncCommand _stopFaceEncoderCommand;
|
||||
private readonly AsyncCommand _uploadFaceEncoderOutputCommand;
|
||||
private readonly AsyncCommand _startFaceMatcherCommand;
|
||||
private readonly AsyncCommand _stopFaceMatcherCommand;
|
||||
private readonly object _faceEncoderProcessLock = new();
|
||||
|
|
@ -75,12 +78,15 @@ namespace ImageCatalog_2
|
|||
private Task? _faceMatcherLogWatcherTask;
|
||||
private bool _hasStartedFaceEncoderInSession;
|
||||
private bool _hasStartedFaceMatcherInSession;
|
||||
private bool _isLoadingFaceSshUserPreferences;
|
||||
private string _lastFaceEncoderOutputFilePath = string.Empty;
|
||||
private int _numberAiGpuRefreshVersion;
|
||||
private volatile bool _numberAiGpuValidationPending;
|
||||
|
||||
private sealed record ParsedFaceMatcherRow(string PhotoId, double? Score, string RawRow, string DebugSummary);
|
||||
|
||||
private const string AiCsvOverwriteDialogTitle = "File CSV gia esistente";
|
||||
private static readonly Regex FaceUploadPathRegex = new(@"^\d{4}/(?:0[1-9]|1[0-2])\.[^/\\]+/[^/\\]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
// ComboBox collections
|
||||
public List<string> AvailableFonts { get; }
|
||||
|
|
@ -90,7 +96,7 @@ namespace ImageCatalog_2
|
|||
[CLSCompliant(false)]
|
||||
public DataModel(ITestService testService, ISettingsService settingsService,
|
||||
ImageCreationService imageCreationService, IAiExtractionService aiExtractionService, IImageProcessingCoordinator imageProcessingCoordinator, PicSettings picSettings,
|
||||
IMapper mapper, ILogger<DataModel> logger, MaddoShared.IVersionProvider? versionProvider = null)
|
||||
IMapper mapper, ILogger<DataModel> logger, MaddoShared.IVersionProvider? versionProvider = null, PickerPreferenceService? pickerPreferenceService = null)
|
||||
{
|
||||
_service = testService;
|
||||
_logger = logger;
|
||||
|
|
@ -98,6 +104,7 @@ namespace ImageCatalog_2
|
|||
_imageCreationService = imageCreationService;
|
||||
_aiExtractionService = aiExtractionService;
|
||||
_imageProcessingCoordinator = imageProcessingCoordinator;
|
||||
_pickerPreferenceService = pickerPreferenceService;
|
||||
_processing = new ProcessingStateViewModel();
|
||||
_processing.PropertyChanged += OnProcessingPropertyChanged;
|
||||
_paths = new PathSettingsViewModel();
|
||||
|
|
@ -122,10 +129,12 @@ namespace ImageCatalog_2
|
|||
StartAiCommand = new AsyncCommand(StartAiAsync);
|
||||
_startFaceEncoderCommand = new AsyncCommand(RunFaceEncoderAsync, CanRunFaceEncoder);
|
||||
_stopFaceEncoderCommand = new AsyncCommand(() => StopFaceEncoderAsync("Arresto richiesto dall'utente."), CanStopFaceEncoder);
|
||||
_uploadFaceEncoderOutputCommand = new AsyncCommand(UploadFaceEncoderOutputAsync, CanUploadFaceEncoderOutput);
|
||||
_startFaceMatcherCommand = new AsyncCommand(RunFaceMatcherAsync, CanRunFaceMatcher);
|
||||
_stopFaceMatcherCommand = new AsyncCommand(() => StopFaceMatcherAsync("Arresto richiesto dall'utente."), CanStopFaceMatcher);
|
||||
StartFaceEncoderCommand = _startFaceEncoderCommand;
|
||||
StopFaceEncoderCommand = _stopFaceEncoderCommand;
|
||||
UploadFaceEncoderOutputCommand = _uploadFaceEncoderOutputCommand;
|
||||
StartFaceMatcherCommand = _startFaceMatcherCommand;
|
||||
StopFaceMatcherCommand = _stopFaceMatcherCommand;
|
||||
|
||||
|
|
@ -139,6 +148,7 @@ namespace ImageCatalog_2
|
|||
|
||||
// Load available fonts
|
||||
AvailableFonts = LoadAvailableFonts();
|
||||
LoadFaceSshUserPreferences();
|
||||
QueueRefreshNumberAiGpuCapabilities();
|
||||
RefreshFaceExecutableCapabilities();
|
||||
}
|
||||
|
|
@ -330,6 +340,67 @@ namespace ImageCatalog_2
|
|||
set => _ai.FaceOutputFolderPath = value;
|
||||
}
|
||||
|
||||
public string FaceUploadPath
|
||||
{
|
||||
get => _ai.FaceUploadPath;
|
||||
set
|
||||
{
|
||||
_ai.FaceUploadPath = value?.Trim() ?? string.Empty;
|
||||
NotifyPropertyChanged(nameof(IsFaceUploadPathValid));
|
||||
UpdateFaceUploadCommandStates();
|
||||
}
|
||||
}
|
||||
|
||||
public bool FaceUploadDryRun
|
||||
{
|
||||
get => _ai.FaceUploadDryRun;
|
||||
set => SetFaceSshPreferenceBoolValue(PickerPreferenceKeys.FaceUploadDryRun, normalizedValue => _ai.FaceUploadDryRun = normalizedValue, value);
|
||||
}
|
||||
|
||||
public bool IsFaceUploadPathValid => IsValidFaceUploadPath(FaceUploadPath);
|
||||
|
||||
public string FaceSshUsername
|
||||
{
|
||||
get => _ai.FaceSshUsername;
|
||||
set => SetFaceSshPreferenceValue(PickerPreferenceKeys.FaceSshUsername, normalizedValue => _ai.FaceSshUsername = normalizedValue, value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
public string FaceSshPassword
|
||||
{
|
||||
get => _ai.FaceSshPassword;
|
||||
set => SetFaceSshPreferenceValue(PickerPreferenceKeys.FaceSshPassword, normalizedValue => _ai.FaceSshPassword = normalizedValue, value ?? string.Empty);
|
||||
}
|
||||
|
||||
public string FaceSshAddress
|
||||
{
|
||||
get => _ai.FaceSshAddress;
|
||||
set => SetFaceSshPreferenceValue(PickerPreferenceKeys.FaceSshAddress, normalizedValue => _ai.FaceSshAddress = normalizedValue, value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
public string FaceSshPort
|
||||
{
|
||||
get => _ai.FaceSshPort;
|
||||
set => SetFaceSshPreferenceValue(PickerPreferenceKeys.FaceSshPort, normalizedValue => _ai.FaceSshPort = normalizedValue, value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
public string FaceSshPathA
|
||||
{
|
||||
get => _ai.FaceSshPathA;
|
||||
set => SetFaceSshPreferenceValue(PickerPreferenceKeys.FaceSshPathA, normalizedValue => _ai.FaceSshPathA = normalizedValue, value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
public string FaceSshPathB
|
||||
{
|
||||
get => _ai.FaceSshPathB;
|
||||
set => SetFaceSshPreferenceValue(PickerPreferenceKeys.FaceSshPathB, normalizedValue => _ai.FaceSshPathB = normalizedValue, value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
public bool IsFaceUploadRunning
|
||||
{
|
||||
get => _ai.IsFaceUploadRunning;
|
||||
private set => _ai.IsFaceUploadRunning = value;
|
||||
}
|
||||
|
||||
public bool FaceRecursive
|
||||
{
|
||||
get => _ai.FaceRecursive;
|
||||
|
|
@ -668,7 +739,13 @@ namespace ImageCatalog_2
|
|||
}
|
||||
|
||||
NotifyPropertyChanged(e.PropertyName);
|
||||
if (string.Equals(e.PropertyName, nameof(AiSettingsViewModel.FaceUploadPath), StringComparison.Ordinal))
|
||||
{
|
||||
NotifyPropertyChanged(nameof(IsFaceUploadPathValid));
|
||||
}
|
||||
|
||||
UpdateFaceEncoderCommandStates();
|
||||
UpdateFaceUploadCommandStates();
|
||||
UpdateFaceMatcherCommandStates();
|
||||
}
|
||||
|
||||
|
|
@ -1572,6 +1649,11 @@ namespace ImageCatalog_2
|
|||
_stopFaceEncoderCommand?.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void UpdateFaceUploadCommandStates()
|
||||
{
|
||||
_uploadFaceEncoderOutputCommand?.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void UpdateFaceMatcherCommandStates()
|
||||
{
|
||||
_startFaceMatcherCommand?.RaiseCanExecuteChanged();
|
||||
|
|
@ -1631,6 +1713,7 @@ namespace ImageCatalog_2
|
|||
var parallelism = NormalizeFaceParallelism(FaceParallelism);
|
||||
var minSize = NormalizeFaceMinSize(FaceMinSize);
|
||||
var outputFiles = BuildFaceEncoderOutputPaths(outputFolderPath, imagesFolder, DateTime.Now);
|
||||
_lastFaceEncoderOutputFilePath = outputFiles.OutputFilePath;
|
||||
|
||||
FaceExecutablePath = executableRootPath;
|
||||
FaceOutputFolderPath = outputFolderPath;
|
||||
|
|
@ -1748,6 +1831,66 @@ namespace ImageCatalog_2
|
|||
return !IsFaceMatcherRunning;
|
||||
}
|
||||
|
||||
private bool CanUploadFaceEncoderOutput()
|
||||
{
|
||||
return IsFaceUploadPathValid && !IsFaceUploadRunning;
|
||||
}
|
||||
|
||||
private async Task UploadFaceEncoderOutputAsync()
|
||||
{
|
||||
if (!IsValidFaceUploadPath(FaceUploadPath))
|
||||
{
|
||||
FaceStatusMessage = "Percorso upload non valido.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryBuildFaceUploadRequest(out var request, out var validationMessage))
|
||||
{
|
||||
FaceStatusMessage = validationMessage;
|
||||
await ShowErrorMessageAsync("Upload Face AI", validationMessage).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
IsFaceUploadRunning = true;
|
||||
UpdateFaceUploadCommandStates();
|
||||
FaceStatusMessage = "Upload face encoder in corso...";
|
||||
|
||||
try
|
||||
{
|
||||
if (FaceUploadDryRun)
|
||||
{
|
||||
var preview = BuildFaceUploadDryRunPreview(request);
|
||||
await ShowMessageAsync("Upload Face AI (dry run)", preview).ConfigureAwait(false);
|
||||
await InvokeOnUiThreadAsync(() => FaceStatusMessage = "Dry run: comando mostrato nel popup.").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await RunFaceUploadPowerShellAsync(request).ConfigureAwait(false);
|
||||
await InvokeOnUiThreadAsync(() =>
|
||||
{
|
||||
FaceCommandOutput = string.IsNullOrWhiteSpace(FaceCommandOutput)
|
||||
? result
|
||||
: $"{FaceCommandOutput.TrimEnd()}\n\n{result}";
|
||||
FaceStatusMessage = "Upload completato.";
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = ex.GetBaseException().Message;
|
||||
_logger.LogError(ex, "Face encoder upload failed.");
|
||||
await InvokeOnUiThreadAsync(() => FaceStatusMessage = "Errore durante upload face encoder.").ConfigureAwait(false);
|
||||
await ShowErrorMessageAsync("Upload Face AI", message).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await InvokeOnUiThreadAsync(() =>
|
||||
{
|
||||
IsFaceUploadRunning = false;
|
||||
UpdateFaceUploadCommandStates();
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanStopFaceMatcher()
|
||||
{
|
||||
return IsFaceMatcherRunning;
|
||||
|
|
@ -2361,7 +2504,7 @@ namespace ImageCatalog_2
|
|||
&& File.Exists(configuration.RecognitionWeights);
|
||||
}
|
||||
|
||||
private Task ShowErrorMessageAsync(string title, string message)
|
||||
private Task ShowMessageAsync(string title, string message)
|
||||
{
|
||||
return InvokeOnUiThreadAsync(() =>
|
||||
{
|
||||
|
|
@ -2369,6 +2512,11 @@ namespace ImageCatalog_2
|
|||
});
|
||||
}
|
||||
|
||||
private Task ShowErrorMessageAsync(string title, string message)
|
||||
{
|
||||
return ShowMessageAsync(title, message);
|
||||
}
|
||||
|
||||
internal async Task<bool> ConfirmAiCsvOverwriteIfNeededAsync()
|
||||
{
|
||||
var csvOutputPath = NormalizeFilePathArgument(CsvOutputPath);
|
||||
|
|
@ -2897,6 +3045,253 @@ namespace ImageCatalog_2
|
|||
Path.Combine(outputFolderPath, $"encoder_log_{timestampToken}_{safeRaceName}.txt"));
|
||||
}
|
||||
|
||||
internal static bool IsValidFaceUploadPath(string value)
|
||||
{
|
||||
return FaceUploadPathRegex.IsMatch((value ?? string.Empty).Trim());
|
||||
}
|
||||
|
||||
internal static string CombineRemoteUploadPath(string basePath, string relativePath)
|
||||
{
|
||||
var normalizedBasePath = (basePath ?? string.Empty).Trim().Replace('\\', '/').TrimEnd('/');
|
||||
var normalizedRelativePath = (relativePath ?? string.Empty).Trim().Replace('\\', '/').Trim('/');
|
||||
|
||||
return string.IsNullOrWhiteSpace(normalizedBasePath)
|
||||
? normalizedRelativePath
|
||||
: string.IsNullOrWhiteSpace(normalizedRelativePath)
|
||||
? normalizedBasePath
|
||||
: $"{normalizedBasePath}/{normalizedRelativePath}";
|
||||
}
|
||||
|
||||
private void LoadFaceSshUserPreferences()
|
||||
{
|
||||
if (_pickerPreferenceService is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoadingFaceSshUserPreferences = true;
|
||||
try
|
||||
{
|
||||
_ai.FaceSshUsername = _pickerPreferenceService.GetRememberedValue(PickerPreferenceKeys.FaceSshUsername) ?? string.Empty;
|
||||
_ai.FaceSshPassword = _pickerPreferenceService.GetRememberedRawValue(PickerPreferenceKeys.FaceSshPassword) ?? string.Empty;
|
||||
_ai.FaceSshAddress = _pickerPreferenceService.GetRememberedValue(PickerPreferenceKeys.FaceSshAddress) ?? string.Empty;
|
||||
_ai.FaceSshPort = _pickerPreferenceService.GetRememberedValue(PickerPreferenceKeys.FaceSshPort) ?? "22";
|
||||
_ai.FaceSshPathA = _pickerPreferenceService.GetRememberedValue(PickerPreferenceKeys.FaceSshPathA) ?? string.Empty;
|
||||
_ai.FaceSshPathB = _pickerPreferenceService.GetRememberedValue(PickerPreferenceKeys.FaceSshPathB) ?? string.Empty;
|
||||
_ai.FaceUploadDryRun = TryGetRememberedBoolPreference(PickerPreferenceKeys.FaceUploadDryRun);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoadingFaceSshUserPreferences = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetFaceSshPreferenceValue(string preferenceKey, Action<string> setValue, string value)
|
||||
{
|
||||
setValue(value);
|
||||
if (_isLoadingFaceSshUserPreferences)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pickerPreferenceService?.RememberRawValue(preferenceKey, value);
|
||||
}
|
||||
|
||||
private void SetFaceSshPreferenceBoolValue(string preferenceKey, Action<bool> setValue, bool value)
|
||||
{
|
||||
setValue(value);
|
||||
if (_isLoadingFaceSshUserPreferences)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pickerPreferenceService?.RememberRawValue(preferenceKey, value ? bool.TrueString : bool.FalseString);
|
||||
}
|
||||
|
||||
private bool TryGetRememberedBoolPreference(string preferenceKey)
|
||||
{
|
||||
var rawValue = _pickerPreferenceService?.GetRememberedRawValue(preferenceKey);
|
||||
return bool.TryParse(rawValue, out var parsed) && parsed;
|
||||
}
|
||||
|
||||
private bool TryBuildFaceUploadRequest(out FaceUploadRequest request, out string validationMessage)
|
||||
{
|
||||
request = null!;
|
||||
validationMessage = string.Empty;
|
||||
|
||||
var sourceFilePath = ResolveFaceEncoderUploadSourceFile();
|
||||
if (string.IsNullOrWhiteSpace(sourceFilePath) || !File.Exists(sourceFilePath))
|
||||
{
|
||||
validationMessage = "File .pkl face encoder non trovato nella cartella output.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var username = FaceSshUsername.Trim();
|
||||
var password = FaceSshPassword;
|
||||
var serverAddress = FaceSshAddress.Trim();
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(serverAddress))
|
||||
{
|
||||
validationMessage = "Inserisci username e indirizzo SSH.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!int.TryParse(FaceSshPort.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var port) || port is < 1 or > 65535)
|
||||
{
|
||||
validationMessage = "Porta SSH non valida.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var remoteBasePaths = new[] { FaceSshPathA, FaceSshPathB }
|
||||
.Select(path => path.Trim())
|
||||
.Where(path => !string.IsNullOrWhiteSpace(path))
|
||||
.ToArray();
|
||||
if (remoteBasePaths.Length != 2)
|
||||
{
|
||||
validationMessage = "Inserisci path SSH A e path SSH B.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var relativeUploadPath = FaceUploadPath.Trim();
|
||||
var remoteDirectories = remoteBasePaths
|
||||
.Select(path => CombineRemoteUploadPath(path, relativeUploadPath))
|
||||
.ToArray();
|
||||
|
||||
request = new FaceUploadRequest(sourceFilePath, username, password, serverAddress, port, remoteDirectories);
|
||||
return true;
|
||||
}
|
||||
|
||||
private string? ResolveFaceEncoderUploadSourceFile()
|
||||
{
|
||||
var outputFolderPath = NormalizeDirectoryPathArgument(FaceOutputFolderPath);
|
||||
if (!Directory.Exists(outputFolderPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ResolveLatestFaceUploadSourceFile(outputFolderPath, NormalizeDirectoryPathArgument(DestinationPath));
|
||||
}
|
||||
|
||||
internal static string? ResolveLatestFaceUploadSourceFile(string outputFolderPath, string imagesFolderPath)
|
||||
{
|
||||
var normalizedOutputFolderPath = NormalizeDirectoryPathArgument(outputFolderPath);
|
||||
if (!Directory.Exists(normalizedOutputFolderPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var safeRaceName = BuildSafeFaceEncoderRaceName(NormalizeDirectoryPathArgument(imagesFolderPath));
|
||||
var racePattern = $"face_encodings_*_{safeRaceName}.pkl";
|
||||
|
||||
return new DirectoryInfo(normalizedOutputFolderPath)
|
||||
.EnumerateFiles(racePattern, SearchOption.TopDirectoryOnly)
|
||||
.OrderByDescending(file => file.LastWriteTimeUtc)
|
||||
.ThenByDescending(file => file.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault()
|
||||
?.FullName;
|
||||
}
|
||||
|
||||
private async Task<string> RunFaceUploadPowerShellAsync(FaceUploadRequest request)
|
||||
{
|
||||
var commandLine = BuildFaceUploadPowerShellCommand(request);
|
||||
|
||||
var processStartInfo = CreateFaceUploadProcessStartInfo(commandLine);
|
||||
|
||||
using var process = new Process { StartInfo = processStartInfo };
|
||||
|
||||
if (!process.Start())
|
||||
{
|
||||
throw new InvalidOperationException("Avvio PowerShell per upload fallito.");
|
||||
}
|
||||
|
||||
await process.WaitForExitAsync().ConfigureAwait(false);
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Upload fallito (exit code {process.ExitCode}). Verifica autenticazione SSH o inserisci la password nel terminale se richiesta.");
|
||||
}
|
||||
|
||||
var summary = new StringBuilder();
|
||||
summary.AppendLine("Upload Face AI completato.");
|
||||
summary.AppendLine($"Command: {commandLine}");
|
||||
summary.AppendLine($"File: {request.LocalPath}");
|
||||
foreach (var remoteDirectory in request.RemoteDirectories)
|
||||
{
|
||||
summary.AppendLine($"Destinazione: {remoteDirectory}");
|
||||
}
|
||||
|
||||
return summary.ToString();
|
||||
}
|
||||
|
||||
private static ProcessStartInfo CreateFaceUploadProcessStartInfo(string commandLine)
|
||||
{
|
||||
var processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = ResolvePowerShellExecutable(),
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = false,
|
||||
RedirectStandardError = false,
|
||||
RedirectStandardInput = false,
|
||||
CreateNoWindow = false
|
||||
};
|
||||
|
||||
processStartInfo.ArgumentList.Add("-NoProfile");
|
||||
processStartInfo.ArgumentList.Add("-ExecutionPolicy");
|
||||
processStartInfo.ArgumentList.Add("Bypass");
|
||||
processStartInfo.ArgumentList.Add("-Command");
|
||||
processStartInfo.ArgumentList.Add(commandLine);
|
||||
|
||||
return processStartInfo;
|
||||
}
|
||||
|
||||
private static string BuildFaceUploadDryRunPreview(FaceUploadRequest request)
|
||||
{
|
||||
return BuildFaceUploadPowerShellCommand(request);
|
||||
}
|
||||
|
||||
private static string BuildFaceUploadPowerShellCommand(FaceUploadRequest request)
|
||||
{
|
||||
var fileName = Path.GetFileName(request.LocalPath);
|
||||
var copyCommands = request.RemoteDirectories
|
||||
.Select(remoteDirectory => BuildFaceUploadCopyCommand(request, CombineRemoteUploadPath(remoteDirectory, fileName)))
|
||||
.ToArray();
|
||||
|
||||
return string.Join("; ", copyCommands);
|
||||
}
|
||||
|
||||
private static string ResolvePowerShellExecutable()
|
||||
{
|
||||
return OperatingSystem.IsWindows() ? "powershell.exe" : "pwsh";
|
||||
}
|
||||
|
||||
private static string BuildFaceUploadCopyCommand(FaceUploadRequest request, string remoteFilePath)
|
||||
{
|
||||
return BuildScpCommand(request, remoteFilePath);
|
||||
}
|
||||
|
||||
private static string BuildScpCommand(FaceUploadRequest request, string remoteFilePath)
|
||||
{
|
||||
var remoteTarget = $"{request.UserName}@{request.ServerAddress}:{QuoteScpRemotePath(remoteFilePath)}";
|
||||
return $"scp -P {request.Port.ToString(CultureInfo.InvariantCulture)} {QuotePowerShellString(request.LocalPath)} {QuotePowerShellString(remoteTarget)}";
|
||||
}
|
||||
|
||||
private static string QuotePowerShellString(string value)
|
||||
{
|
||||
return $"'{(value ?? string.Empty).Replace("'", "''", StringComparison.Ordinal)}'";
|
||||
}
|
||||
|
||||
private static string QuoteScpRemotePath(string value)
|
||||
{
|
||||
return "'" + (value ?? string.Empty).Replace("'", "'\\''", StringComparison.Ordinal) + "'";
|
||||
}
|
||||
|
||||
private sealed record FaceUploadRequest(
|
||||
string LocalPath,
|
||||
string UserName,
|
||||
string Password,
|
||||
string ServerAddress,
|
||||
int Port,
|
||||
IReadOnlyList<string> RemoteDirectories);
|
||||
|
||||
internal static string BuildSafeFaceEncoderRaceName(string imagesFolderPath)
|
||||
{
|
||||
var raceName = new DirectoryInfo(imagesFolderPath).Name;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue