diff --git a/src/AppCommon/FileTime.App.Core/Command/Commands.cs b/src/AppCommon/FileTime.App.Core/Command/Commands.cs index c80ec8e..12cf525 100644 --- a/src/AppCommon/FileTime.App.Core/Command/Commands.cs +++ b/src/AppCommon/FileTime.App.Core/Command/Commands.cs @@ -8,12 +8,14 @@ namespace FileTime.App.Core.Command ChangeTimelineMode, CloseTab, Copy, + CopyHash, CopyPath, CreateContainer, CreateElement, Cut, Edit, EnterRapidTravel, + GetHash, GoToHome, GoToPath, GoToProvider, diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/UI/ConsoleInputInterface.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/UI/ConsoleInputInterface.cs index 300c997..1e115c1 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/UI/ConsoleInputInterface.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/UI/ConsoleInputInterface.cs @@ -24,7 +24,7 @@ namespace FileTime.ConsoleUI.App.UI foreach (var input in fields) { _application.MoveToIOLine(); - _coloredConsoleRenderer.Write(input.Text + ": "); + _coloredConsoleRenderer.Write(input.Label + ": "); results.Add(await _consoleReader.ReadText(placeHolder: input.InputType == InputType.Password ? '*' : null)); } diff --git a/src/Core/FileTime.Core/Interactions/InputElement.cs b/src/Core/FileTime.Core/Interactions/InputElement.cs index 87be4c1..bcdd7ab 100644 --- a/src/Core/FileTime.Core/Interactions/InputElement.cs +++ b/src/Core/FileTime.Core/Interactions/InputElement.cs @@ -2,15 +2,38 @@ namespace FileTime.Core.Interactions { public class InputElement { - public string Text { get; } + public string Label { get; } public InputType InputType { get; } public string? DefaultValue { get; } + public List? Options { get; } - public InputElement(string text, InputType inputType, string? defaultValue = null) + protected InputElement(string text, InputType inputType, string? defaultValue = null) { - Text = text; + Label = text; InputType = inputType; DefaultValue = defaultValue; } + + protected InputElement(string text, InputType inputType, List defaultValue) + { + Label = text; + InputType = inputType; + Options = defaultValue; + } + + public static InputElement ForText(string label, string? defaultValue = null) + { + return new InputElement(label, InputType.Text, defaultValue); + } + + public static InputElement ForPassword(string label, string? defaultValue = null) + { + return new InputElement(label, InputType.Password, defaultValue); + } + + public static InputElement ForOptions(string label, List defaultValue) + { + return new InputElement(label, InputType.Options, defaultValue); + } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Interactions/InputType.cs b/src/Core/FileTime.Core/Interactions/InputType.cs index 6eee3d4..c29b7ef 100644 --- a/src/Core/FileTime.Core/Interactions/InputType.cs +++ b/src/Core/FileTime.Core/Interactions/InputType.cs @@ -2,8 +2,8 @@ namespace FileTime.Core.Interactions { public enum InputType { - Text, + Options, Password, - Bool + Text, } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Providers/ContentProviderStream.cs b/src/Core/FileTime.Core/Providers/ContentProviderStream.cs new file mode 100644 index 0000000..25127ff --- /dev/null +++ b/src/Core/FileTime.Core/Providers/ContentProviderStream.cs @@ -0,0 +1,61 @@ +using System.Threading.Tasks; +namespace FileTime.Core.Providers +{ + public class ContentProviderStream : Stream + { + + private readonly IContentReader? _contentReader; + private readonly IContentWriter? _contentWriter; + public override bool CanRead => _contentReader == null; + + public override bool CanSeek => false; + + public override bool CanWrite => _contentWriter == null; + + public override long Length => throw new NotImplementedException(); + + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public ContentProviderStream(IContentReader contentReader) + { + _contentReader = contentReader; + } + + public ContentProviderStream(IContentWriter contentWriter) + { + _contentWriter = contentWriter; + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (_contentReader == null) throw new IOException("This stream is not readable"); + var dataTask = Task.Run(async () => await _contentReader.ReadBytesAsync(count, offset)); + dataTask.Wait(); + var data = dataTask.Result; + + if (data.Length > count) throw new Exception("More bytes has been read than requested"); + Array.Copy(data, buffer, data.Length); + return data.Length; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Providers/IContentReader.cs b/src/Core/FileTime.Core/Providers/IContentReader.cs index bf06fcb..c77c31e 100644 --- a/src/Core/FileTime.Core/Providers/IContentReader.cs +++ b/src/Core/FileTime.Core/Providers/IContentReader.cs @@ -4,6 +4,6 @@ namespace FileTime.Core.Providers { int PreferredBufferSize { get; } - Task ReadBytesAsync(int bufferSize); + Task ReadBytesAsync(int bufferSize, int? offset = null); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/CommandTimeState.cs b/src/Core/FileTime.Core/Timeline/CommandTimeState.cs index 1763e01..ff8b3e6 100644 --- a/src/Core/FileTime.Core/Timeline/CommandTimeState.cs +++ b/src/Core/FileTime.Core/Timeline/CommandTimeState.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using FileTime.Core.Command; namespace FileTime.Core.Timeline @@ -12,7 +13,7 @@ namespace FileTime.Core.Timeline public CommandTimeState(ICommand command, PointInTime? startTime) { Command = command; - UpdateState(startTime).Wait(); + Task.Run(async () => await UpdateState(startTime)).Wait(); } public async Task UpdateState(PointInTime? startPoint) diff --git a/src/Core/FileTime.Core/Timeline/TimeRunner.cs b/src/Core/FileTime.Core/Timeline/TimeRunner.cs index 86154f1..c83702f 100644 --- a/src/Core/FileTime.Core/Timeline/TimeRunner.cs +++ b/src/Core/FileTime.Core/Timeline/TimeRunner.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using AsyncEvent; using FileTime.Core.Command; using FileTime.Core.Models; @@ -136,7 +137,7 @@ namespace FileTime.Core.Timeline if (arg is CommandTimeState commandToRun2) { commandToRun = commandToRun2; - _commandExecutor.ExecuteCommandAsync(commandToRun.Command, this).Wait(); + Task.Run(async () => await _commandExecutor.ExecuteCommandAsync(commandToRun.Command, this)).Wait(); } } catch (Exception e) @@ -150,7 +151,7 @@ namespace FileTime.Core.Timeline } finally { - DisposeCommandThread(Thread.CurrentThread, commandToRun).Wait(); + Task.Run(async () => await DisposeCommandThread(Thread.CurrentThread, commandToRun)).Wait(); } } @@ -238,6 +239,6 @@ namespace FileTime.Core.Timeline } } - private void RunWithLock(Action action) => RunWithLockAsync(action).Wait(); + private void RunWithLock(Action action) => Task.Run(async () => await RunWithLockAsync(action)).Wait(); } } \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/App.axaml b/src/GuiApp/FileTime.Avalonia/App.axaml index 2c1b90d..68ab626 100644 --- a/src/GuiApp/FileTime.Avalonia/App.axaml +++ b/src/GuiApp/FileTime.Avalonia/App.axaml @@ -202,5 +202,25 @@ + + + diff --git a/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs b/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs index 0b0d01f..be5d140 100644 --- a/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs +++ b/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs @@ -55,7 +55,7 @@ namespace FileTime.Avalonia.Application {*/ /*var task = SetSelectedItemAsync(value, true); Task.WaitAll(new Task[] { task }, 100);*/ - SetSelectedItemAsync(value, true); + Task.Run(async () => await SetSelectedItemAsync(value, true)).Wait(); /*} catch { diff --git a/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs b/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs index 85ef45e..49aace9 100644 --- a/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs +++ b/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs @@ -46,6 +46,7 @@ namespace FileTime.Avalonia.Configuration new CommandBindingConfiguration(Commands.ChangeTimelineMode, new[] { Key.T, Key.M }), new CommandBindingConfiguration(Commands.CloseTab, Key.Q), new CommandBindingConfiguration(Commands.Copy, new[] { Key.Y, Key.Y }), + new CommandBindingConfiguration(Commands.CopyHash, new[] { Key.C, Key.H }), new CommandBindingConfiguration(Commands.CopyPath, new[] { Key.C, Key.P }), new CommandBindingConfiguration(Commands.CreateContainer, Key.F7), new CommandBindingConfiguration(Commands.CreateContainer, new[] { Key.C, Key.C }), diff --git a/src/GuiApp/FileTime.Avalonia/Misc/InputElementWrapper.cs b/src/GuiApp/FileTime.Avalonia/Misc/InputElementWrapper.cs index f81ce59..3327265 100644 --- a/src/GuiApp/FileTime.Avalonia/Misc/InputElementWrapper.cs +++ b/src/GuiApp/FileTime.Avalonia/Misc/InputElementWrapper.cs @@ -8,6 +8,8 @@ namespace FileTime.Avalonia.Misc public string Value { get; set; } + public object? Option { get; set; } + public char? PasswordChar { get; set; } public InputElementWrapper(InputElement inputElement, string? defaultValue = null) diff --git a/src/GuiApp/FileTime.Avalonia/Models/HashFunction.cs b/src/GuiApp/FileTime.Avalonia/Models/HashFunction.cs new file mode 100644 index 0000000..033ec13 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Models/HashFunction.cs @@ -0,0 +1,10 @@ +namespace FileTime.Avalonia.Models +{ + public enum HashFunction + { + MD5, + SHA256, + SHA384, + SHA512, + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs b/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs index 62c01fd..8a1400a 100644 --- a/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs +++ b/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs @@ -2,12 +2,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Security.Cryptography; using System.Threading.Tasks; using FileTime.App.Core.Clipboard; using FileTime.App.Core.Command; using FileTime.Avalonia.Application; using FileTime.Avalonia.IconProviders; using FileTime.Avalonia.Misc; +using FileTime.Avalonia.Models; using FileTime.Avalonia.ViewModels; using FileTime.Core.Command; using FileTime.Core.Components; @@ -65,6 +67,7 @@ namespace FileTime.Avalonia.Services {Commands.ChangeTimelineMode, ChangeTimelineMode}, {Commands.CloseTab, CloseTab}, {Commands.Copy, Copy}, + {Commands.CopyHash, CopyHash}, {Commands.CopyPath, CopyPath}, {Commands.CreateContainer, CreateContainer}, {Commands.CreateElement, CreateElement}, @@ -276,13 +279,13 @@ namespace FileTime.Avalonia.Services var createContainerCommand = new CreateContainerCommand(new AbsolutePath(container), containerName); await AddCommand(createContainerCommand); } - catch(Exception e) + catch (Exception e) { _logger.LogError(e, "Error while creating container {Container}", containerName); } }; - _dialogService.ReadInputs(new List() { new InputElement("Container name", InputType.Text) }, handler); + _dialogService.ReadInputs(new List() { InputElement.ForText("Container name") }, handler); return Task.CompletedTask; } @@ -299,13 +302,13 @@ namespace FileTime.Avalonia.Services var createElementCommand = new CreateElementCommand(new AbsolutePath(container), elementName); await AddCommand(createElementCommand); } - catch(Exception e) + catch (Exception e) { _logger.LogError(e, "Error while creating element {Element}", elementName); } }; - _dialogService.ReadInputs(new List() { new InputElement("Element name", InputType.Text) }, handler); + _dialogService.ReadInputs(new List() { InputElement.ForText("Element name") }, handler); return Task.CompletedTask; } @@ -487,7 +490,7 @@ namespace FileTime.Avalonia.Services await AddCommand(renameCommand); }; - _dialogService.ReadInputs(new List() { new InputElement("New name", InputType.Text, selectedItem.Name) }, handler); + _dialogService.ReadInputs(new List() { InputElement.ForText("New name", selectedItem.Name) }, handler); } return Task.CompletedTask; } @@ -543,7 +546,7 @@ namespace FileTime.Avalonia.Services } }; - _dialogService.ReadInputs(new List() { new InputElement("Path", InputType.Text) }, handler); + _dialogService.ReadInputs(new List() { InputElement.ForText("Path") }, handler); return Task.CompletedTask; } @@ -580,9 +583,17 @@ namespace FileTime.Avalonia.Services var currentContainer = _appState.SelectedTab.CurrentLocation.Container; var textToCopy = currentContainer.NativePath; - if (textToCopy != null && global::Avalonia.Application.Current?.Clipboard is global::Avalonia.Input.Platform.IClipboard clipboard) + if (textToCopy != null) { - await clipboard.SetTextAsync(textToCopy); + await CopyToClipboard(textToCopy); + } + } + + private static async Task CopyToClipboard(string text) + { + if (global::Avalonia.Application.Current?.Clipboard is global::Avalonia.Input.Platform.IClipboard clipboard) + { + await clipboard.SetTextAsync(text); } } @@ -633,7 +644,7 @@ namespace FileTime.Avalonia.Services return Task.CompletedTask; }; - _dialogService.ReadInputs(new List() { new InputElement("Command", InputType.Text) }, handler); + _dialogService.ReadInputs(new List() { InputElement.ForText("Command") }, handler); return Task.CompletedTask; } @@ -840,5 +851,40 @@ namespace FileTime.Avalonia.Services //TODO: else return Task.CompletedTask; } + + private Task CopyHash() + { + var handler = async (List inputs) => + { + var hashFunction = (HashFunction?)inputs[0].Option; + if (hashFunction != null && _appState.SelectedTab.SelectedItem?.Item is IElement element && element.Provider.SupportsContentStreams) + { + using var stream = new ContentProviderStream(await element.GetContentReaderAsync()); + + string? hashString = null; + using HashAlgorithm hashFunc = hashFunction switch + { + HashFunction.MD5 => MD5.Create(), + HashFunction.SHA256 => SHA256.Create(), + HashFunction.SHA384 => SHA384.Create(), + HashFunction.SHA512 => SHA512.Create(), + _ => throw new NotImplementedException() + }; + + var hash = hashFunc.ComputeHash(stream); + hashString = string.Concat(hash.Select(b => b.ToString("X2"))); + + _dialogService.ShowToastMessage($"Hash copied ({hashString})"); + await CopyToClipboard(hashString); + } + }; + + _dialogService.ReadInputs(new List() + { + InputElement.ForOptions("Hash function", Enum.GetValues().Cast().ToList()) + }, handler); + + return Task.CompletedTask; + } } } \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml index a8c468b..c0fd3af 100644 --- a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml +++ b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml @@ -5,6 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:FileTime.Avalonia.ViewModels" + xmlns:interactions="using:FileTime.Core.Interactions" xmlns:local="using:FileTime.Avalonia.Views" xmlns:models="using:FileTime.Avalonia.Models" Title="FileTime" @@ -470,20 +471,35 @@ Items="{Binding AppState.Inputs}"> - + - + VerticalAlignment="Top" + Text="{Binding InputElement.Label}" /> + + + + + diff --git a/src/Providers/FileTime.Providers.Local/LocalContentReader.cs b/src/Providers/FileTime.Providers.Local/LocalContentReader.cs index 698392a..a79487d 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentReader.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentReader.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using FileTime.Core.Providers; namespace FileTime.Providers.Local @@ -9,6 +10,7 @@ namespace FileTime.Providers.Local private bool disposed; public int PreferredBufferSize => 1024 * 1024; + private long? _bytesRead; public LocalContentReader(FileStream readerStream) { @@ -16,11 +18,27 @@ namespace FileTime.Providers.Local _binaryReader = new BinaryReader(_readerStream); } - public Task ReadBytesAsync(int bufferSize) + public Task ReadBytesAsync(int bufferSize, int? offset = null) { var max = bufferSize > 0 && bufferSize < PreferredBufferSize ? bufferSize : PreferredBufferSize; - return Task.FromResult(_binaryReader.ReadBytes(max)); + if (offset != null) + { + if (_bytesRead == null) _bytesRead = 0; + var buffer = new byte[max]; + var bytesRead = _binaryReader.Read(buffer, offset.Value, max); + _bytesRead += bytesRead; + + if (buffer.Length != bytesRead) + { + Array.Resize(ref buffer, bytesRead); + } + return Task.FromResult(buffer); + } + else + { + return Task.FromResult(_binaryReader.ReadBytes(max)); + } } ~LocalContentReader() diff --git a/src/Providers/FileTime.Providers.Smb/SmbContentReader.cs b/src/Providers/FileTime.Providers.Smb/SmbContentReader.cs index 72db473..898ae6a 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbContentReader.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbContentReader.cs @@ -21,11 +21,11 @@ namespace FileTime.Providers.Smb _client = client; } - public Task ReadBytesAsync(int bufferSize) + public Task ReadBytesAsync(int bufferSize, int? offset = null) { var max = bufferSize > 0 && bufferSize < (int)_client.MaxReadSize ? bufferSize : (int)_client.MaxReadSize; - var status = _smbFileStore.ReadFile(out byte[] data, _fileHandle, _bytesRead, max); + var status = _smbFileStore.ReadFile(out byte[] data, _fileHandle, offset ?? _bytesRead, max); if (status != NTStatus.STATUS_SUCCESS && status != NTStatus.STATUS_END_OF_FILE) { throw new Exception("Failed to read from file"); diff --git a/src/Providers/FileTime.Providers.Smb/SmbServer.cs b/src/Providers/FileTime.Providers.Smb/SmbServer.cs index b9a94ca..a6538d7 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbServer.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbServer.cs @@ -195,8 +195,8 @@ namespace FileTime.Providers.Smb var inputs = await _inputInterface.ReadInputs( new InputElement[] { - new InputElement($"Username for '{Name}'", InputType.Text, Username ?? ""), - new InputElement($"Password for '{Name}'", InputType.Password, Password ?? "") + InputElement.ForText($"Username for '{Name}'", Username ?? ""), + InputElement.ForPassword($"Password for '{Name}'", Password ?? "") }); Username = inputs[0];