// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // // Filename : SubdirectoryViewModel.cs // Author(s) : Natalia Portillo // // Component : GUI view models. // // --[ Description ] ---------------------------------------------------------- // // View model and code for the subdirectory contents panel. // // --[ 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-2025 Natalia Portillo // ****************************************************************************/ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows.Input; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; using Aaru.CommonTypes.Interop; using Aaru.CommonTypes.Structs; using Aaru.Core; using Aaru.Gui.Models; using Aaru.Localization; using Aaru.Logging; using Avalonia.Controls; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.Input; using JetBrains.Annotations; using MsBox.Avalonia; using MsBox.Avalonia.Enums; using Sentry; using FileAttributes = Aaru.CommonTypes.Structs.FileAttributes; namespace Aaru.Gui.ViewModels.Panels; public sealed class SubdirectoryViewModel { readonly SubdirectoryModel _model; readonly Window _view; public SubdirectoryViewModel([NotNull] SubdirectoryModel model, Window view) { Entries = []; SelectedEntries = []; ExtractFilesCommand = new AsyncRelayCommand(ExtractFiles); _model = model; _view = view; ErrorNumber errno = model.Plugin.OpenDir(model.Path, out IDirNode node); if(errno != ErrorNumber.NoError) { _ = MessageBoxManager.GetMessageBoxStandard(UI.Title_Error, string.Format(UI.Error_0_trying_to_read_1_of_chosen_filesystem, errno, model.Path), ButtonEnum.Ok, Icon.Error) .ShowWindowDialogAsync(view); return; } while(model.Plugin.ReadDir(node, out string dirent) == ErrorNumber.NoError && dirent is not null) { errno = model.Plugin.Stat(model.Path + "/" + dirent, out FileEntryInfo stat); if(errno != ErrorNumber.NoError) { AaruLogging.Error(string.Format(UI.Error_0_trying_to_get_information_about_filesystem_entry_named_1, errno, dirent)); continue; } if(stat.Attributes.HasFlag(FileAttributes.Directory) && !model.Listed) { model.Subdirectories.Add(new SubdirectoryModel { Name = dirent, Path = model.Path + "/" + dirent, Plugin = model.Plugin }); continue; } Entries.Add(new FileModel { Name = dirent, Stat = stat }); } model.Plugin.CloseDir(node); } public ObservableCollection Entries { get; } public List SelectedEntries { get; } public ICommand ExtractFilesCommand { get; } async Task ExtractFiles() { if(SelectedEntries.Count == 0) return; IReadOnlyList result = await _view.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = UI.Dialog_Choose_destination_folder, AllowMultiple = false }); if(result.Count != 1) return; Statistics.AddCommand("extract-files"); string folder = result[0].Path.AbsolutePath; foreach(FileModel file in SelectedEntries) { string filename = file.Name; ButtonResult mboxResult; if(DetectOS.IsWindows) { if(filename.Contains('<') || filename.Contains('>') || filename.Contains(':') || filename.Contains('\\') || filename.Contains('/') || filename.Contains('|') || filename.Contains('?') || filename.Contains('*') || filename.Any(c => c < 32) || filename.Equals("CON", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("PRN", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("AUX", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("COM1", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("COM2", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("COM3", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("COM4", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("COM5", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("COM6", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("COM7", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("COM8", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("COM9", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("LPT1", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("LPT2", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("LPT3", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("LPT4", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("LPT5", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("LPT6", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("LPT7", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("LPT8", StringComparison.InvariantCultureIgnoreCase) || filename.Equals("LPT9", StringComparison.InvariantCultureIgnoreCase) || filename.Last() == '.' || filename.Last() == ' ') { char[] chars; if(filename.Last() == '.' || filename.Last() == ' ') chars = new char[filename.Length - 1]; else chars = new char[filename.Length]; for(var ci = 0; ci < chars.Length; ci++) { chars[ci] = filename[ci] switch { '<' or '>' or ':' or '\\' or '/' or '|' or '?' or '*' or '\u0000' or '\u0001' or '\u0002' or '\u0003' or '\u0004' or '\u0005' or '\u0006' or '\u0007' or '\u0008' or '\u0009' or '\u000A' or '\u000B' or '\u000C' or '\u000D' or '\u000E' or '\u000F' or '\u0010' or '\u0011' or '\u0012' or '\u0013' or '\u0014' or '\u0015' or '\u0016' or '\u0017' or '\u0018' or '\u0019' or '\u001A' or '\u001B' or '\u001C' or '\u001D' or '\u001E' or '\u001F' => '_', _ => filename[ci] }; } if(filename.StartsWith("CON", StringComparison.InvariantCultureIgnoreCase) || filename.StartsWith("PRN", StringComparison.InvariantCultureIgnoreCase) || filename.StartsWith("AUX", StringComparison.InvariantCultureIgnoreCase) || filename.StartsWith("COM", StringComparison.InvariantCultureIgnoreCase) || filename.StartsWith("LPT", StringComparison.InvariantCultureIgnoreCase)) { chars[0] = '_'; chars[1] = '_'; chars[2] = '_'; } string corrected = new(chars); mboxResult = await MessageBoxManager.GetMessageBoxStandard(UI.Unsupported_filename, string .Format(UI .Filename_0_not_supported_want_to_rename_to_1, filename, corrected), ButtonEnum.YesNoCancel, Icon.Warning) .ShowWindowDialogAsync(_view); switch(mboxResult) { case ButtonResult.Cancel: return; case ButtonResult.No: continue; default: filename = corrected; break; } } } string outputPath = Path.Combine(folder, filename); if(File.Exists(outputPath)) { mboxResult = await MessageBoxManager.GetMessageBoxStandard(UI.Existing_file, string .Format(UI.File_named_0_exists_overwrite_Q, filename), ButtonEnum.YesNoCancel, Icon.Warning) .ShowWindowDialogAsync(_view); switch(mboxResult) { case ButtonResult.Cancel: return; case ButtonResult.No: continue; default: try { File.Delete(outputPath); } catch(IOException) { mboxResult = await MessageBoxManager.GetMessageBoxStandard(UI.Cannot_delete, UI.Could_note_delete_existe_file_continue_Q, ButtonEnum.YesNo, Icon.Error) .ShowWindowDialogAsync(_view); if(mboxResult == ButtonResult.No) return; } break; } } try { var outBuf = new byte[file.Stat.Length]; ErrorNumber error = _model.Plugin.OpenFile(_model.Path + "/" + file.Name, out IFileNode fileNode); if(error == ErrorNumber.NoError) { error = _model.Plugin.ReadFile(fileNode, file.Stat.Length, outBuf, out _); _model.Plugin.CloseFile(fileNode); } if(error != ErrorNumber.NoError) { mboxResult = await MessageBoxManager.GetMessageBoxStandard(UI.Error_reading_file, string .Format(UI .Error_0_reading_file_continue_Q, error), ButtonEnum.YesNo, Icon.Error) .ShowWindowDialogAsync(_view); if(mboxResult == ButtonResult.No) return; continue; } var fs = new FileStream(outputPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None); fs.Write(outBuf, 0, outBuf.Length); fs.Close(); var fi = new FileInfo(outputPath); try { if(file.Stat.CreationTimeUtc.HasValue) fi.CreationTimeUtc = file.Stat.CreationTimeUtc.Value; } catch(Exception ex) { SentrySdk.CaptureException(ex); } try { if(file.Stat.LastWriteTimeUtc.HasValue) fi.LastWriteTimeUtc = file.Stat.LastWriteTimeUtc.Value; } catch(Exception ex) { SentrySdk.CaptureException(ex); } try { if(file.Stat.AccessTimeUtc.HasValue) fi.LastAccessTimeUtc = file.Stat.AccessTimeUtc.Value; } catch(Exception ex) { SentrySdk.CaptureException(ex); } } catch(IOException) { mboxResult = await MessageBoxManager.GetMessageBoxStandard(UI.Cannot_create_file, UI .Could_not_create_destination_file_continue_Q, ButtonEnum.YesNo, Icon.Error) .ShowWindowDialogAsync(_view); if(mboxResult == ButtonResult.No) return; } } } }