diff --git a/.idea/.idea.Aaru/.idea/contentModel.xml b/.idea/.idea.Aaru/.idea/contentModel.xml index 7ec2970e5..58d148504 100644 --- a/.idea/.idea.Aaru/.idea/contentModel.xml +++ b/.idea/.idea.Aaru/.idea/contentModel.xml @@ -1234,6 +1234,7 @@ + @@ -1249,6 +1250,7 @@ + @@ -1258,10 +1260,10 @@ + + - - @@ -1314,6 +1316,7 @@ + diff --git a/Aaru.Gui/Forms/frmMain.xeto.cs b/Aaru.Gui/Forms/frmMain.xeto.cs index 9fa64bd66..38260a644 100644 --- a/Aaru.Gui/Forms/frmMain.xeto.cs +++ b/Aaru.Gui/Forms/frmMain.xeto.cs @@ -362,46 +362,6 @@ namespace Aaru.Gui.Forms lblError.Text = devErrorMessage; splMain.Panel2 = lblError; - break; - case Dictionary files: - splMain.Panel2 = new pnlListFiles(selectedItem.Values[2] as IReadOnlyFilesystem, files, - selectedItem.Values[1] as string == "/" ? "/" - : selectedItem.Values[4] as string); - - break; - case null when selectedItem.Values.Length >= 5 && selectedItem.Values[4] is string dirPath && - selectedItem.Values[2] is IReadOnlyFilesystem fsPlugin: - Errno errno = fsPlugin.ReadDir(dirPath, out List dirents); - - if(errno != Errno.NoError) - { - Eto.Forms.MessageBox.Show($"Error {errno} trying to read \"{dirPath}\" of chosen filesystem", - MessageBoxType.Error); - - break; - } - - Dictionary filesNew = new Dictionary(); - - foreach(string dirent in dirents) - { - errno = fsPlugin.Stat(dirPath + "/" + dirent, out FileEntryInfo stat); - - if(errno != Errno.NoError) - { - AaruConsole. - ErrorWriteLine($"Error {errno} trying to get information about filesystem entry named {dirent}"); - - continue; - } - - if(!stat.Attributes.HasFlag(FileAttributes.Directory)) - filesNew.Add(dirent, stat); - } - - selectedItem.Values[3] = filesNew; - splMain.Panel2 = new pnlListFiles(fsPlugin, filesNew, dirPath); - break; } } diff --git a/Aaru.Gui/Models/FileModel.cs b/Aaru.Gui/Models/FileModel.cs new file mode 100644 index 000000000..057a8b0db --- /dev/null +++ b/Aaru.Gui/Models/FileModel.cs @@ -0,0 +1,26 @@ +using System; +using Aaru.CommonTypes.Structs; + +namespace Aaru.Gui.Models +{ + public class FileModel + { + public string Name { get; set; } + public string Size => $"{Stat.Length}"; + public string CreationTime => + Stat.CreationTime == default(DateTime) ? "" : $"{Stat.CreationTime:G}"; + public string LastAccessTime => Stat.AccessTime == default(DateTime) ? "" : $"{Stat.AccessTime:G}"; + public string ChangedTime => + Stat.StatusChangeTime == default(DateTime) ? "" : $"{Stat.StatusChangeTime:G}"; + public string LastBackupTime => Stat.BackupTime == default(DateTime) ? "" : $"{Stat.BackupTime:G}"; + public string LastWriteTime => + Stat.LastWriteTime == default(DateTime) ? "" : $"{Stat.LastWriteTime:G}"; + public string Attributes => $"{Stat.Attributes}"; + public string Gid => $"{Stat.GID}"; + public string Uid => $"{Stat.UID}"; + public string Inode => $"{Stat.Inode}"; + public string Links => $"{Stat.Links}"; + public string Mode => $"{Stat.Mode}"; + public FileEntryInfo Stat { get; set; } + } +} \ No newline at end of file diff --git a/Aaru.Gui/Models/FileSystemModel.cs b/Aaru.Gui/Models/FileSystemModel.cs index e15362c61..f85758c3f 100644 --- a/Aaru.Gui/Models/FileSystemModel.cs +++ b/Aaru.Gui/Models/FileSystemModel.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using Aaru.CommonTypes.Interfaces; using Aaru.Gui.ViewModels; @@ -5,9 +6,12 @@ namespace Aaru.Gui.Models { public class FileSystemModel : RootModel { - public string VolumeName { get; set; } - public IFilesystem Filesystem { get; set; } - public IReadOnlyFilesystem ReadOnlyFilesystem { get; set; } - public FileSystemViewModel ViewModel { get; set; } + public FileSystemModel() => Roots = new ObservableCollection(); + + public string VolumeName { get; set; } + public IFilesystem Filesystem { get; set; } + public IReadOnlyFilesystem ReadOnlyFilesystem { get; set; } + public FileSystemViewModel ViewModel { get; set; } + public ObservableCollection Roots { get; set; } } } \ No newline at end of file diff --git a/Aaru.Gui/Models/SubdirectoryModel.cs b/Aaru.Gui/Models/SubdirectoryModel.cs new file mode 100644 index 000000000..30fbc939c --- /dev/null +++ b/Aaru.Gui/Models/SubdirectoryModel.cs @@ -0,0 +1,16 @@ +using System.Collections.ObjectModel; +using Aaru.CommonTypes.Interfaces; + +namespace Aaru.Gui.Models +{ + public class SubdirectoryModel + { + public SubdirectoryModel() => Subdirectories = new ObservableCollection(); + + public string Name { get; set; } + public string Path { get; set; } + public ObservableCollection Subdirectories { get; set; } + public IReadOnlyFilesystem Plugin { get; set; } + public bool Listed { get; set; } + } +} \ No newline at end of file diff --git a/Aaru.Gui/Panels/SubdirectoryPanel.xaml b/Aaru.Gui/Panels/SubdirectoryPanel.xaml new file mode 100644 index 000000000..26264a097 --- /dev/null +++ b/Aaru.Gui/Panels/SubdirectoryPanel.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Aaru.Gui/Panels/SubdirectoryPanel.xaml.cs b/Aaru.Gui/Panels/SubdirectoryPanel.xaml.cs new file mode 100644 index 000000000..5c71f18fe --- /dev/null +++ b/Aaru.Gui/Panels/SubdirectoryPanel.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Aaru.Gui.Panels +{ + public class SubdirectoryPanel : UserControl + { + public SubdirectoryPanel() => InitializeComponent(); + + void InitializeComponent() => AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/Aaru.Gui/Panels/pnlListFiles.xeto b/Aaru.Gui/Panels/pnlListFiles.xeto deleted file mode 100644 index ca1431716..000000000 --- a/Aaru.Gui/Panels/pnlListFiles.xeto +++ /dev/null @@ -1,36 +0,0 @@ - - - - \ No newline at end of file diff --git a/Aaru.Gui/Panels/pnlListFiles.xeto.cs b/Aaru.Gui/Panels/pnlListFiles.xeto.cs deleted file mode 100644 index 3eeaa0d44..000000000 --- a/Aaru.Gui/Panels/pnlListFiles.xeto.cs +++ /dev/null @@ -1,552 +0,0 @@ -// /*************************************************************************** -// Aaru Data Preservation Suite -// ---------------------------------------------------------------------------- -// -// Filename : pnlListFiles.xeto.cs -// Author(s) : Natalia Portillo -// -// Component : List files panel. -// -// --[ Description ] ---------------------------------------------------------- -// -// Implements the list files 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-2020 Natalia Portillo -// ****************************************************************************/ - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using Aaru.CommonTypes.Interfaces; -using Aaru.CommonTypes.Interop; -using Aaru.CommonTypes.Structs; -using Aaru.Core; -using Eto.Forms; -using Eto.Serialization.Xaml; - -namespace Aaru.Gui.Panels -{ - // TODO: Resize columns - // TODO: File icons? - // TODO: Show xattrs - public class pnlListFiles : Panel - { - readonly GridColumn accessColumn; - readonly GridColumn attributesColumn; - readonly GridColumn backupColumn; - readonly GridColumn changedColumn; - readonly GridColumn createdColumn; - readonly ObservableCollection entries; - readonly IReadOnlyFilesystem filesystem; - readonly GridColumn gidColumn; - - readonly GridColumn inodeColumn; - readonly GridColumn linksColumn; - readonly GridColumn modeColumn; - readonly GridColumn nameColumn; - readonly ButtonMenuItem saveFilesMenuItem; - readonly GridColumn sizeColumn; - readonly GridColumn uidColumn; - readonly GridColumn writeColumn; - bool ascendingSort; - - #region XAML controls - #pragma warning disable 169 - #pragma warning disable 649 - GridView grdFiles; - #pragma warning restore 169 - #pragma warning restore 649 - #endregion - GridColumn sortedColumn; - - public pnlListFiles(IReadOnlyFilesystem filesystem, Dictionary files, string parentPath) - { - this.filesystem = filesystem; - XamlReader.Load(this); - - entries = new ObservableCollection(); - - nameColumn = new GridColumn - { - DataCell = new TextBoxCell - { - Binding = Binding.Property(r => r.Name) - }, - HeaderText = "Name", Sortable = true - }; - - sizeColumn = new GridColumn - { - DataCell = new TextBoxCell - { - Binding = Binding.Property(r => $"{r.Stat.Length}") - }, - HeaderText = "Size", Sortable = true - }; - - createdColumn = new GridColumn - { - DataCell = new TextBoxCell - { - Binding = Binding.Property(r => r.Stat.CreationTime == default(DateTime) ? "" - : $"{r.Stat.CreationTime:G}") - }, - HeaderText = "Created", Sortable = true - }; - - accessColumn = new GridColumn - { - DataCell = new TextBoxCell - { - Binding = Binding.Property(r => r.Stat.AccessTime == default(DateTime) ? "" - : $"{r.Stat.AccessTime:G}") - }, - HeaderText = "Last access", Sortable = true - }; - - changedColumn = new GridColumn - { - DataCell = new TextBoxCell - { - Binding = Binding.Property(r => r.Stat.StatusChangeTime == default(DateTime) - ? "" : $"{r.Stat.StatusChangeTime:G}") - }, - HeaderText = "Changed", Sortable = true - }; - - backupColumn = new GridColumn - { - DataCell = new TextBoxCell - { - Binding = Binding.Property(r => r.Stat.BackupTime == default(DateTime) ? "" - : $"{r.Stat.BackupTime:G}") - }, - HeaderText = "Last backup", Sortable = true - }; - - writeColumn = new GridColumn - { - DataCell = new TextBoxCell - { - Binding = Binding.Property(r => r.Stat.LastWriteTime == default(DateTime) ? "" - : $"{r.Stat.LastWriteTime:G}") - }, - HeaderText = "Last write", Sortable = true - }; - - attributesColumn = new GridColumn - { - DataCell = new TextBoxCell - { - Binding = Binding.Property(r => $"{r.Stat.Attributes}") - }, - HeaderText = "Attributes", Sortable = true - }; - - gidColumn = new GridColumn - { - DataCell = new TextBoxCell - { - Binding = Binding.Property(r => $"{r.Stat.GID}") - }, - HeaderText = "GID", Sortable = true - }; - - uidColumn = new GridColumn - { - DataCell = new TextBoxCell - { - Binding = Binding.Property(r => $"{r.Stat.UID}") - }, - HeaderText = "UID", Sortable = true - }; - - inodeColumn = new GridColumn - { - DataCell = new TextBoxCell - { - Binding = Binding.Property(r => $"{r.Stat.Inode}") - }, - HeaderText = "Inode", Sortable = true - }; - - linksColumn = new GridColumn - { - DataCell = new TextBoxCell - { - Binding = Binding.Property(r => $"{r.Stat.Links}") - }, - HeaderText = "Links", Sortable = true - }; - - modeColumn = new GridColumn - { - DataCell = new TextBoxCell - { - Binding = Binding.Property(r => $"{r.Stat.Mode}") - }, - HeaderText = "Mode", Sortable = true - }; - - grdFiles.Columns.Add(nameColumn); - grdFiles.Columns.Add(sizeColumn); - grdFiles.Columns.Add(createdColumn); - grdFiles.Columns.Add(accessColumn); - grdFiles.Columns.Add(changedColumn); - grdFiles.Columns.Add(backupColumn); - grdFiles.Columns.Add(writeColumn); - grdFiles.Columns.Add(attributesColumn); - grdFiles.Columns.Add(gidColumn); - grdFiles.Columns.Add(uidColumn); - grdFiles.Columns.Add(inodeColumn); - grdFiles.Columns.Add(linksColumn); - grdFiles.Columns.Add(modeColumn); - - grdFiles.AllowColumnReordering = true; - grdFiles.AllowDrop = false; - grdFiles.AllowMultipleSelection = true; - grdFiles.ShowHeader = true; - - foreach(KeyValuePair file in files) - entries.Add(new EntryForGrid - { - Name = file.Key, Stat = file.Value, ParentPath = parentPath - }); - - grdFiles.DataStore = entries; - sortedColumn = null; - grdFiles.ColumnHeaderClick += OnGrdFilesOnColumnHeaderClick; - ascendingSort = true; - - grdFiles.ContextMenu = new ContextMenu(); - - saveFilesMenuItem = new ButtonMenuItem - { - Text = "Extract to...", Enabled = false - }; - - saveFilesMenuItem.Click += OnSaveFilesMenuItemClick; - - grdFiles.ContextMenu.Items.Add(saveFilesMenuItem); - - grdFiles.SelectionChanged += OnGrdFilesSelectionChanged; - } - - void OnGrdFilesSelectionChanged(object sender, EventArgs e) => - saveFilesMenuItem.Enabled = grdFiles.SelectedItems.Any(); - - void OnSaveFilesMenuItemClick(object sender, EventArgs e) - { - if(!grdFiles.SelectedItems.Any()) - return; - - var saveFilesFolderDialog = new SelectFolderDialog - { - Title = "Choose destination folder..." - }; - - DialogResult result = saveFilesFolderDialog.ShowDialog(this); - - if(result != DialogResult.Ok) - return; - - Statistics.AddCommand("extract-files"); - - string folder = saveFilesFolderDialog.Directory; - - foreach(EntryForGrid file in grdFiles.SelectedItems) - { - string filename = file.Name; - - 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.ToUpperInvariant() == "CON" || - filename.ToUpperInvariant() == "PRN" || - filename.ToUpperInvariant() == "AUX" || - filename.ToUpperInvariant() == "COM1" || - filename.ToUpperInvariant() == "COM2" || - filename.ToUpperInvariant() == "COM3" || - filename.ToUpperInvariant() == "COM4" || - filename.ToUpperInvariant() == "COM5" || - filename.ToUpperInvariant() == "COM6" || - filename.ToUpperInvariant() == "COM7" || - filename.ToUpperInvariant() == "COM8" || - filename.ToUpperInvariant() == "COM9" || - filename.ToUpperInvariant() == "LPT1" || - filename.ToUpperInvariant() == "LPT2" || - filename.ToUpperInvariant() == "LPT3" || - filename.ToUpperInvariant() == "LPT4" || - filename.ToUpperInvariant() == "LPT5" || - filename.ToUpperInvariant() == "LPT6" || - filename.ToUpperInvariant() == "LPT7" || - filename.ToUpperInvariant() == "LPT8" || - filename.ToUpperInvariant() == "LPT9" || - filename.Last() == '.' || - filename.Last() == ' ') - { - char[] chars; - - if(filename.Last() == '.' || - filename.Last() == ' ') - chars = new char[filename.Length - 1]; - else - chars = new char[filename.Length]; - - for(int ci = 0; ci < chars.Length; ci++) - switch(filename[ci]) - { - case '<': - case '>': - case ':': - case '\\': - case '/': - case '|': - case '?': - case '*': - case '\u0000': - case '\u0001': - case '\u0002': - case '\u0003': - case '\u0004': - case '\u0005': - case '\u0006': - case '\u0007': - case '\u0008': - case '\u0009': - case '\u000A': - case '\u000B': - case '\u000C': - case '\u000D': - case '\u000E': - case '\u000F': - case '\u0010': - case '\u0011': - case '\u0012': - case '\u0013': - case '\u0014': - case '\u0015': - case '\u0016': - case '\u0017': - case '\u0018': - case '\u0019': - case '\u001A': - case '\u001B': - case '\u001C': - case '\u001D': - case '\u001E': - case '\u001F': - chars[ci] = '_'; - - break; - default: - chars[ci] = filename[ci]; - - break; - } - - 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 string(chars); - - result = Eto.Forms.MessageBox.Show(this, "Unsupported filename", - $"The file name {filename} is not supported on this platform.\nDo you want to rename it to {corrected}?", - MessageBoxButtons.YesNoCancel, MessageBoxType.Warning); - - if(result == DialogResult.Cancel) - return; - - if(result == DialogResult.No) - continue; - - filename = corrected; - } - - string outputPath = Path.Combine(folder, filename); - - if(File.Exists(outputPath)) - { - result = Eto.Forms.MessageBox.Show(this, "Existing file", - $"A file named {filename} already exists on the destination folder.\nDo you want to overwrite it?", - MessageBoxButtons.YesNoCancel, MessageBoxType.Question); - - if(result == DialogResult.Cancel) - return; - - if(result == DialogResult.No) - continue; - - try - { - File.Delete(outputPath); - } - catch(IOException) - { - result = Eto.Forms.MessageBox.Show(this, "Cannot delete", - "Could not delete existing file.\nDo you want to continue?", - MessageBoxButtons.YesNo, MessageBoxType.Warning); - - if(result == DialogResult.No) - return; - } - } - - try - { - byte[] outBuf = new byte[0]; - - Errno error = filesystem.Read(file.ParentPath + file.Name, 0, file.Stat.Length, ref outBuf); - - if(error != Errno.NoError) - { - result = Eto.Forms.MessageBox.Show(this, "Error reading file", - $"Error {error} reading file.\nDo you want to continue?", - MessageBoxButtons.YesNo, MessageBoxType.Warning); - - if(result == DialogResult.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); - #pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body - try - { - if(file.Stat.CreationTimeUtc.HasValue) - fi.CreationTimeUtc = file.Stat.CreationTimeUtc.Value; - } - catch - { - // ignored - } - - try - { - if(file.Stat.LastWriteTimeUtc.HasValue) - fi.LastWriteTimeUtc = file.Stat.LastWriteTimeUtc.Value; - } - catch - { - // ignored - } - - try - { - if(file.Stat.AccessTimeUtc.HasValue) - fi.LastAccessTimeUtc = file.Stat.AccessTimeUtc.Value; - } - catch - { - // ignored - } - #pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body - } - catch(IOException) - { - result = Eto.Forms.MessageBox.Show(this, "Cannot create file", - "Could not create destination file.\nDo you want to continue?", - MessageBoxButtons.YesNo, MessageBoxType.Warning); - - if(result == DialogResult.No) - return; - } - } - } - - void OnGrdFilesOnColumnHeaderClick(object sender, GridColumnEventArgs gridColumnEventArgs) - { - if(sortedColumn == gridColumnEventArgs.Column) - ascendingSort = !ascendingSort; - else - ascendingSort = true; - - sortedColumn = gridColumnEventArgs.Column; - - if(sortedColumn == nameColumn) - grdFiles.DataStore = ascendingSort ? entries.OrderBy(t => t.Name) - : entries.OrderByDescending(t => t.Name); - else if(sortedColumn == sizeColumn) - grdFiles.DataStore = ascendingSort ? entries.OrderBy(t => t.Stat.Length) - : entries.OrderByDescending(t => t.Stat.Length); - else if(sortedColumn == createdColumn) - grdFiles.DataStore = ascendingSort ? entries.OrderBy(t => t.Stat.CreationTime) - : entries.OrderByDescending(t => t.Stat.CreationTime); - else if(sortedColumn == accessColumn) - grdFiles.DataStore = ascendingSort ? entries.OrderBy(t => t.Stat.AccessTime) - : entries.OrderByDescending(t => t.Stat.AccessTime); - else if(sortedColumn == changedColumn) - grdFiles.DataStore = ascendingSort ? entries.OrderBy(t => t.Stat.StatusChangeTime) - : entries.OrderByDescending(t => t.Stat.StatusChangeTime); - else if(sortedColumn == backupColumn) - grdFiles.DataStore = ascendingSort ? entries.OrderBy(t => t.Stat.BackupTime) - : entries.OrderByDescending(t => t.Stat.BackupTime); - else if(sortedColumn == writeColumn) - grdFiles.DataStore = ascendingSort ? entries.OrderBy(t => t.Stat.LastWriteTime) - : entries.OrderByDescending(t => t.Stat.LastWriteTime); - else if(sortedColumn == attributesColumn) - grdFiles.DataStore = ascendingSort ? entries.OrderBy(t => t.Stat.Attributes) - : entries.OrderByDescending(t => t.Stat.Attributes); - else if(sortedColumn == gidColumn) - grdFiles.DataStore = ascendingSort ? entries.OrderBy(t => t.Stat.GID) - : entries.OrderByDescending(t => t.Stat.GID); - else if(sortedColumn == uidColumn) - grdFiles.DataStore = ascendingSort ? entries.OrderBy(t => t.Stat.UID) - : entries.OrderByDescending(t => t.Stat.UID); - else if(sortedColumn == inodeColumn) - grdFiles.DataStore = ascendingSort ? entries.OrderBy(t => t.Stat.Inode) - : entries.OrderByDescending(t => t.Stat.Inode); - else if(sortedColumn == linksColumn) - grdFiles.DataStore = ascendingSort ? entries.OrderBy(t => t.Stat.Links) - : entries.OrderByDescending(t => t.Stat.Links); - else if(sortedColumn == modeColumn) - grdFiles.DataStore = ascendingSort ? entries.OrderBy(t => t.Stat.Mode) - : entries.OrderByDescending(t => t.Stat.Mode); - } - - class EntryForGrid - { - public string ParentPath; - public FileEntryInfo Stat; - public string Name { get; set; } - } - } -} \ No newline at end of file diff --git a/Aaru.Gui/ViewModels/MainWindowViewModel.cs b/Aaru.Gui/ViewModels/MainWindowViewModel.cs index ebd4c997d..5ebbf7431 100644 --- a/Aaru.Gui/ViewModels/MainWindowViewModel.cs +++ b/Aaru.Gui/ViewModels/MainWindowViewModel.cs @@ -160,6 +160,14 @@ namespace Aaru.Gui.ViewModels DataContext = fileSystemModel.ViewModel }; + if(value is SubdirectoryModel subdirectoryModel) + { + ContentPanel = new SubdirectoryPanel + { + DataContext = new SubdirectoryViewModel(subdirectoryModel, _view) + }; + } + this.RaiseAndSetIfChanged(ref _treeViewSelectedItem, value); } } @@ -474,10 +482,16 @@ namespace Aaru.Gui.ViewModels ViewModel = new FileSystemViewModel(plugin.XmlFsType, information) }; - /* TODO: Trap expanding item + // TODO: Trap expanding item if(fsPlugin != null) + { + filesystemModel.Roots.Add(new SubdirectoryModel + { + Name = "/", Path = "", Plugin = fsPlugin + }); + Statistics.AddCommand("ls"); - */ + } Statistics.AddFilesystem(plugin.XmlFsType.Type); partitionModel.FileSystems.Add(filesystemModel); @@ -531,10 +545,16 @@ namespace Aaru.Gui.ViewModels ViewModel = new FileSystemViewModel(plugin.XmlFsType, information) }; - /* TODO: Trap expanding item + // TODO: Trap expanding item if(fsPlugin != null) + { + filesystemModel.Roots.Add(new SubdirectoryModel + { + Name = "/", Path = "", Plugin = fsPlugin + }); + Statistics.AddCommand("ls"); - */ + } Statistics.AddFilesystem(plugin.XmlFsType.Type); imageModel.PartitionSchemesOrFileSystems.Add(filesystemModel); diff --git a/Aaru.Gui/ViewModels/SubdirectoryViewModel.cs b/Aaru.Gui/ViewModels/SubdirectoryViewModel.cs new file mode 100644 index 000000000..5da889f95 --- /dev/null +++ b/Aaru.Gui/ViewModels/SubdirectoryViewModel.cs @@ -0,0 +1,326 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive; +using Aaru.CommonTypes.Interop; +using Aaru.CommonTypes.Structs; +using Aaru.Console; +using Aaru.Core; +using Aaru.Gui.Models; +using Avalonia.Controls; +using MessageBox.Avalonia; +using MessageBox.Avalonia.Enums; +using ReactiveUI; +using FileAttributes = Aaru.CommonTypes.Structs.FileAttributes; + +namespace Aaru.Gui.ViewModels +{ + public class SubdirectoryViewModel + { + readonly SubdirectoryModel _model; + readonly Window _view; + + public SubdirectoryViewModel(SubdirectoryModel model, Window view) + { + Entries = new ObservableCollection(); + SelectedEntries = new List(); + ExtractFilesCommand = ReactiveCommand.Create(ExecuteExtractFilesCommand); + _model = model; + _view = view; + + Errno errno = model.Plugin.ReadDir(model.Path, out List dirents); + + if(errno != Errno.NoError) + { + MessageBoxManager. + GetMessageBoxStandardWindow("Error", + $"Error {errno} trying to read \"{model.Path}\" of chosen filesystem", + ButtonEnum.Ok, Icon.Error).ShowDialog(view); + + return; + } + + foreach(string dirent in dirents) + { + errno = model.Plugin.Stat(model.Path + "/" + dirent, out FileEntryInfo stat); + + if(errno != Errno.NoError) + { + AaruConsole. + ErrorWriteLine($"Error {errno} trying to get information about filesystem entry named {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 + }); + } + } + + public ObservableCollection Entries { get; } + public List SelectedEntries { get; } + public ReactiveCommand ExtractFilesCommand { get; } + + async void ExecuteExtractFilesCommand() + { + if(SelectedEntries.Count == 0) + return; + + ButtonResult mboxResult; + + var saveFilesFolderDialog = new OpenFolderDialog + { + Title = "Choose destination folder..." + }; + + string result = await saveFilesFolderDialog.ShowAsync(_view); + + if(result is null) + return; + + Statistics.AddCommand("extract-files"); + + string folder = saveFilesFolderDialog.Directory; + + foreach(FileModel file in SelectedEntries) + { + string filename = file.Name; + + 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.ToUpperInvariant() == "CON" || + filename.ToUpperInvariant() == "PRN" || + filename.ToUpperInvariant() == "AUX" || + filename.ToUpperInvariant() == "COM1" || + filename.ToUpperInvariant() == "COM2" || + filename.ToUpperInvariant() == "COM3" || + filename.ToUpperInvariant() == "COM4" || + filename.ToUpperInvariant() == "COM5" || + filename.ToUpperInvariant() == "COM6" || + filename.ToUpperInvariant() == "COM7" || + filename.ToUpperInvariant() == "COM8" || + filename.ToUpperInvariant() == "COM9" || + filename.ToUpperInvariant() == "LPT1" || + filename.ToUpperInvariant() == "LPT2" || + filename.ToUpperInvariant() == "LPT3" || + filename.ToUpperInvariant() == "LPT4" || + filename.ToUpperInvariant() == "LPT5" || + filename.ToUpperInvariant() == "LPT6" || + filename.ToUpperInvariant() == "LPT7" || + filename.ToUpperInvariant() == "LPT8" || + filename.ToUpperInvariant() == "LPT9" || + filename.Last() == '.' || + filename.Last() == ' ') + { + char[] chars; + + if(filename.Last() == '.' || + filename.Last() == ' ') + chars = new char[filename.Length - 1]; + else + chars = new char[filename.Length]; + + for(int ci = 0; ci < chars.Length; ci++) + switch(filename[ci]) + { + case '<': + case '>': + case ':': + case '\\': + case '/': + case '|': + case '?': + case '*': + case '\u0000': + case '\u0001': + case '\u0002': + case '\u0003': + case '\u0004': + case '\u0005': + case '\u0006': + case '\u0007': + case '\u0008': + case '\u0009': + case '\u000A': + case '\u000B': + case '\u000C': + case '\u000D': + case '\u000E': + case '\u000F': + case '\u0010': + case '\u0011': + case '\u0012': + case '\u0013': + case '\u0014': + case '\u0015': + case '\u0016': + case '\u0017': + case '\u0018': + case '\u0019': + case '\u001A': + case '\u001B': + case '\u001C': + case '\u001D': + case '\u001E': + case '\u001F': + chars[ci] = '_'; + + break; + default: + chars[ci] = filename[ci]; + + break; + } + + 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 string(chars); + + mboxResult = await MessageBoxManager.GetMessageBoxStandardWindow("Unsupported filename", + $"The file name {filename} is not supported on this platform.\nDo you want to rename it to {corrected}?", + ButtonEnum.YesNoCancel, + Icon.Warning). + ShowDialog(_view); + + if(mboxResult == ButtonResult.Cancel) + return; + + if(mboxResult == ButtonResult.No) + continue; + + filename = corrected; + } + + string outputPath = Path.Combine(folder, filename); + + if(File.Exists(outputPath)) + { + mboxResult = await MessageBoxManager.GetMessageBoxStandardWindow("Existing file", + $"A file named {filename} already exists on the destination folder.\nDo you want to overwrite it?", + ButtonEnum.YesNoCancel, + Icon.Warning).ShowDialog(_view); + + if(mboxResult == ButtonResult.Cancel) + return; + + if(mboxResult == ButtonResult.No) + continue; + + try + { + File.Delete(outputPath); + } + catch(IOException) + { + mboxResult = await MessageBoxManager.GetMessageBoxStandardWindow("Cannot delete", + "Could not delete existing file.\nDo you want to continue?", + ButtonEnum.YesNo, Icon.Error). + ShowDialog(_view); + + if(mboxResult == ButtonResult.No) + return; + } + } + + try + { + byte[] outBuf = new byte[0]; + + Errno error = _model.Plugin.Read(_model.Path + "/" + file.Name, 0, file.Stat.Length, ref outBuf); + + if(error != Errno.NoError) + { + mboxResult = await MessageBoxManager.GetMessageBoxStandardWindow("Error reading file", + $"Error {error} reading file.\nDo you want to continue?", + ButtonEnum.YesNo, Icon.Error). + ShowDialog(_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); + #pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body + try + { + if(file.Stat.CreationTimeUtc.HasValue) + fi.CreationTimeUtc = file.Stat.CreationTimeUtc.Value; + } + catch + { + // ignored + } + + try + { + if(file.Stat.LastWriteTimeUtc.HasValue) + fi.LastWriteTimeUtc = file.Stat.LastWriteTimeUtc.Value; + } + catch + { + // ignored + } + + try + { + if(file.Stat.AccessTimeUtc.HasValue) + fi.LastAccessTimeUtc = file.Stat.AccessTimeUtc.Value; + } + catch + { + // ignored + } + #pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body + } + catch(IOException) + { + mboxResult = await MessageBoxManager.GetMessageBoxStandardWindow("Cannot create file", + "Could not create destination file.\nDo you want to continue?", + ButtonEnum.YesNo, Icon.Error). + ShowDialog(_view); + + if(mboxResult == ButtonResult.No) + return; + } + } + } + } +} \ No newline at end of file diff --git a/Aaru.Gui/Views/MainWindow.xaml b/Aaru.Gui/Views/MainWindow.xaml index fe72ad2a4..0d858d4fe 100644 --- a/Aaru.Gui/Views/MainWindow.xaml +++ b/Aaru.Gui/Views/MainWindow.xaml @@ -54,13 +54,27 @@ - - - - - - - + + + + + + + @@ -79,12 +93,18 @@ - + + + + + + + diff --git a/Aaru.sln.DotSettings b/Aaru.sln.DotSettings index 25ca023e1..1c167c52e 100644 --- a/Aaru.sln.DotSettings +++ b/Aaru.sln.DotSettings @@ -212,6 +212,7 @@ True True True + True True True True