diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Application.CommandHandlers.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Application.CommandHandlers.cs index fbeb0ef..d53241e 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Application.CommandHandlers.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Application.CommandHandlers.cs @@ -169,8 +169,16 @@ namespace FileTime.ConsoleUI.App { IList? itemsToDelete = null; - var currentSelectedItems = (await _tabStates[_selectedTab!].GetCurrentMarkedItems()).Select(p => p.ResolveAsync()).ToList(); - var currentSelectedItem = await _selectedTab?.GetCurrentSelectedItem(); + var currentSelectedItems = new List(); + foreach (var item in await _tabStates[_selectedTab!].GetCurrentMarkedItems()) + { + var resolvedItem = await item.ResolveAsync(); + if (resolvedItem != null) currentSelectedItems.Add(resolvedItem); + } + + IItem? currentSelectedItem = null; + if (_selectedTab != null) currentSelectedItem = await _selectedTab.GetCurrentSelectedItem(); + if (currentSelectedItems.Count > 0) { var delete = true; diff --git a/src/Core/FileTime.Core/Command/CopyCommand.cs b/src/Core/FileTime.Core/Command/CopyCommand.cs index 789d83e..1bfe3a4 100644 --- a/src/Core/FileTime.Core/Command/CopyCommand.cs +++ b/src/Core/FileTime.Core/Command/CopyCommand.cs @@ -60,9 +60,9 @@ namespace FileTime.Core.Command var newDiffs = new List(); - _copyOperation = (_, to, _, _) => + _copyOperation = async (_, to, _, _) => { - var target = to.GetParent().ResolveAsync(); + var target = await to.GetParent().ResolveAsync(); newDiffs.Add(new Difference( target is IElement ? DifferenceItemType.Element @@ -70,8 +70,6 @@ namespace FileTime.Core.Command DifferenceActionType.Create, to )); - - return Task.CompletedTask; }; _createContainer = async (IContainer target, string name) => @@ -188,6 +186,7 @@ namespace FileTime.Core.Command for (var i = 0; targetNameExists; i++) { targetName = element.Name + (i == 0 ? "_" : $"_{i}"); + targetNameExists = await target.IsExistsAsync(targetName); } } else if (transportMode == Command.TransportMode.Skip && targetNameExists) diff --git a/src/Core/FileTime.Core/Command/CreateElementCommand.cs b/src/Core/FileTime.Core/Command/CreateElementCommand.cs index 422d051..c685a6a 100644 --- a/src/Core/FileTime.Core/Command/CreateElementCommand.cs +++ b/src/Core/FileTime.Core/Command/CreateElementCommand.cs @@ -43,7 +43,7 @@ namespace FileTime.Core.Command public async Task CanRun(PointInTime startPoint) { - var resolvedContainer = Container.ResolveAsync(); + var resolvedContainer = await Container.ResolveAsync(); if (resolvedContainer == null) return CanCommandRun.Forceable; if (resolvedContainer is not IContainer container diff --git a/src/Core/FileTime.Core/Command/RenameCommand.cs b/src/Core/FileTime.Core/Command/RenameCommand.cs index 1d7a1c6..b7a161c 100644 --- a/src/Core/FileTime.Core/Command/RenameCommand.cs +++ b/src/Core/FileTime.Core/Command/RenameCommand.cs @@ -48,9 +48,9 @@ namespace FileTime.Core.Command return startPoint.WithDifferences(newDifferences); } - public Task CanRun(PointInTime startPoint) + public async Task CanRun(PointInTime startPoint) { - return Task.FromResult(Source.ResolveAsync() != null ? CanCommandRun.True : CanCommandRun.False); + return await Source.ResolveAsync() != null ? CanCommandRun.True : CanCommandRun.False; } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/CommandHandlers/StreamCopyCommandHandler.cs b/src/Core/FileTime.Core/CommandHandlers/StreamCopyCommandHandler.cs index d09e137..7263c03 100644 --- a/src/Core/FileTime.Core/CommandHandlers/StreamCopyCommandHandler.cs +++ b/src/Core/FileTime.Core/CommandHandlers/StreamCopyCommandHandler.cs @@ -41,10 +41,13 @@ namespace FileTime.Core.CommandHandlers do { dataRead = await reader.ReadBytesAsync(writer.PreferredBufferSize); - await writer.WriteBytesAsync(dataRead); - await writer.FlushAsync(); - if (operationProgress != null) operationProgress.Progress += dataRead.LongLength; - await copyCommandContext.UpdateProgress(); + if (dataRead.Length > 0) + { + await writer.WriteBytesAsync(dataRead); + await writer.FlushAsync(); + if (operationProgress != null) operationProgress.Progress += dataRead.LongLength; + await copyCommandContext.UpdateProgress(); + } } while (dataRead.Length > 0); } diff --git a/src/Core/FileTime.Core/Components/Tab.cs b/src/Core/FileTime.Core/Components/Tab.cs index 641ba32..9149bf1 100644 --- a/src/Core/FileTime.Core/Components/Tab.cs +++ b/src/Core/FileTime.Core/Components/Tab.cs @@ -45,7 +45,8 @@ namespace FileTime.Core.Components _currentLocation = value; await CurrentLocationChanged.InvokeAsync(this, AsyncEventArgs.Empty); - var currentLocationItems = (await (await GetCurrentLocation()).GetItems())!; + var currentLocationItems = await (await GetCurrentLocation()).GetItems(); + if (currentLocationItems == null) throw new Exception("Could not get current location items."); await SetCurrentSelectedItem(await GetItemByLastPath() ?? (currentLocationItems.Count > 0 ? currentLocationItems[0] : null)); _currentLocation.Refreshed.Add(HandleCurrentLocationRefresh); } @@ -155,11 +156,11 @@ namespace FileTime.Core.Components var currentLocationItems = (await (await GetCurrentLocation(token)).GetItems(token))!; if (currentSelectedName != null) { - await SetCurrentSelectedItem(currentLocationItems.FirstOrDefault(i => i.FullName == currentSelectedName) ?? currentLocationItems[0]); + await SetCurrentSelectedItem(currentLocationItems.FirstOrDefault(i => i.FullName == currentSelectedName) ?? (currentLocationItems.Count > 0 ? currentLocationItems[0] : null), token: token); } else if (currentLocationItems.Count > 0) { - await SetCurrentSelectedItem(currentLocationItems[0]); + await SetCurrentSelectedItem(currentLocationItems[0], token: token); } } diff --git a/src/Core/FileTime.Core/Models/IContainer.cs b/src/Core/FileTime.Core/Models/IContainer.cs index d835e9d..9e62f41 100644 --- a/src/Core/FileTime.Core/Models/IContainer.cs +++ b/src/Core/FileTime.Core/Models/IContainer.cs @@ -23,7 +23,12 @@ namespace FileTime.Core.Models if (item is IContainer container) { - var result = await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)), acceptDeepestMatch); + IItem? result = null; + try + { + result = await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)), acceptDeepestMatch); + } + catch { } return result == null && acceptDeepestMatch ? this : result; } diff --git a/src/Core/FileTime.Core/Timeline/TimeRunner.cs b/src/Core/FileTime.Core/Timeline/TimeRunner.cs index 9b8528d..86154f1 100644 --- a/src/Core/FileTime.Core/Timeline/TimeRunner.cs +++ b/src/Core/FileTime.Core/Timeline/TimeRunner.cs @@ -139,9 +139,14 @@ namespace FileTime.Core.Timeline _commandExecutor.ExecuteCommandAsync(commandToRun.Command, this).Wait(); } } - catch(Exception e) + catch (Exception e) { - _logger.LogError(e, "Error while running command: {CommandType} ({Command}).", commandToRun?.Command.GetType().Name, commandToRun?.Command.DisplayLabel); + _logger.LogError( + e, + "Error while running command: {CommandType} ({Command}) {Error}.", + commandToRun?.Command.GetType().Name, + commandToRun?.Command.DisplayLabel, + e.Message); } finally { diff --git a/src/GuiApp/FileTime.Avalonia/Converters/ExceptionToStringConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/ExceptionToStringConverter.cs index b1aeda9..33ecb6e 100644 --- a/src/GuiApp/FileTime.Avalonia/Converters/ExceptionToStringConverter.cs +++ b/src/GuiApp/FileTime.Avalonia/Converters/ExceptionToStringConverter.cs @@ -11,7 +11,27 @@ namespace FileTime.Avalonia.Converters if (value is not Exception e) return value; if (e is UnauthorizedAccessException) return e.Message; + else if (e.InnerException != null) + { + return TraverseInnerException(e); + } + return FormatException(e); + } + + private static string TraverseInnerException(Exception e) + { + string s = ""; + if (e.InnerException != null) s += TraverseInnerException(e.InnerException) + Environment.NewLine; + else return FormatException(e); + + s += "In: " + FormatException(e); + + return s; + } + + private static string FormatException(Exception e) + { return $"{e.Message} ({e.GetType().FullName})"; } diff --git a/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs b/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs index 730171a..62c01fd 100644 --- a/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs +++ b/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs @@ -268,9 +268,18 @@ namespace FileTime.Avalonia.Services { var handler = async (List inputs) => { - var container = _appState.SelectedTab.CurrentLocation.Container; - var createContainerCommand = new CreateContainerCommand(new AbsolutePath(container), inputs[0].Value); - await AddCommand(createContainerCommand); + string? containerName = null; + try + { + containerName = inputs[0].Value; + var container = _appState.SelectedTab.CurrentLocation.Container; + var createContainerCommand = new CreateContainerCommand(new AbsolutePath(container), containerName); + await AddCommand(createContainerCommand); + } + catch(Exception e) + { + _logger.LogError(e, "Error while creating container {Container}", containerName); + } }; _dialogService.ReadInputs(new List() { new InputElement("Container name", InputType.Text) }, handler); @@ -282,9 +291,18 @@ namespace FileTime.Avalonia.Services { var handler = async (List inputs) => { - var container = _appState.SelectedTab.CurrentLocation.Container; - var createElementCommand = new CreateElementCommand(new AbsolutePath(container), inputs[0].Value); - await AddCommand(createElementCommand); + string? elementName = null; + try + { + elementName = inputs[0].Value; + var container = _appState.SelectedTab.CurrentLocation.Container; + var createElementCommand = new CreateElementCommand(new AbsolutePath(container), elementName); + await AddCommand(createElementCommand); + } + catch(Exception e) + { + _logger.LogError(e, "Error while creating element {Element}", elementName); + } }; _dialogService.ReadInputs(new List() { new InputElement("Element name", InputType.Text) }, handler); diff --git a/src/GuiApp/FileTime.Avalonia/Services/KeyInputHandlerService.cs b/src/GuiApp/FileTime.Avalonia/Services/KeyInputHandlerService.cs index 9f85dcd..ee32e27 100644 --- a/src/GuiApp/FileTime.Avalonia/Services/KeyInputHandlerService.cs +++ b/src/GuiApp/FileTime.Avalonia/Services/KeyInputHandlerService.cs @@ -207,7 +207,7 @@ namespace FileTime.Avalonia.Services } catch (Exception e) { - _logger.LogError(e, "Unknown error while running commnad. {Command}", command); + _logger.LogError(e, "Unknown error while running command. {Command} {Error}", command, e); } } diff --git a/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs b/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs index 81eb316..a27a9de 100644 --- a/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs +++ b/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs @@ -137,7 +137,19 @@ namespace FileTime.Avalonia.Services if (container == null) continue; var newTab = new Tab(); - await newTab.Init(container); + while (true) + { + try + { + if (container == null) throw new Exception($"Could not find an initializable path along {tab.Path}"); + await newTab.Init(container); + break; + } + catch + { + container = container!.GetParent(); + } + } var newTabContainer = new TabContainer(newTab, _localContentProvider, _itemNameConverterService); await newTabContainer.Init(tab.Number); diff --git a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml index d9e0ce6..a8c468b 100644 --- a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml +++ b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml @@ -323,7 +323,7 @@ - + diff --git a/src/Providers/FileTime.Providers.Smb/SmbClientContext.cs b/src/Providers/FileTime.Providers.Smb/SmbClientContext.cs index 26f2fa5..5f2fa8f 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbClientContext.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbClientContext.cs @@ -4,24 +4,29 @@ namespace FileTime.Providers.Smb { public class SmbClientContext { - private readonly Func> _getSmbClient; + private readonly Func> _getSmbClient; private readonly Action _disposeClient; private bool _isRunning; private readonly object _lock = new(); - public SmbClientContext(Func> getSmbClient, Action disposeClient) + public SmbClientContext(Func> getSmbClient, Action disposeClient) { _getSmbClient = getSmbClient; _disposeClient = disposeClient; } - public async Task RunWithSmbClientAsync(Func func) + public async Task RunWithSmbClientAsync(Action action, int maxRetries = SmbServer.MAXRETRIES) + { + await RunWithSmbClientAsync((client) => { action(client); return null; }, maxRetries); + } + + public async Task RunWithSmbClientAsync(Func func, int maxRetries = SmbServer.MAXRETRIES) { while (true) { lock (_lock) { - if(!_isRunning) + if (!_isRunning) { _isRunning = true; break; @@ -37,7 +42,7 @@ namespace FileTime.Providers.Smb { try { - client = await _getSmbClient(); + client = await _getSmbClient(maxRetries); return func(client); } catch (Exception e) when (e.Source == "SMBLibrary") diff --git a/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs b/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs index a695fe6..0d836f4 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs @@ -94,7 +94,22 @@ namespace FileTime.Providers.Smb } var remainingPath = string.Join(Constants.SeparatorChar, pathParts.Skip(1)); - return remainingPath.Length == 0 ? rootContainer : await rootContainer.GetByPath(remainingPath, acceptDeepestMatch); + try + { + return remainingPath.Length == 0 ? rootContainer : await rootContainer.GetByPath(remainingPath, acceptDeepestMatch); + } + catch (Exception e) + { + _logger.LogError(e, "Error while getting path {Path}", path); + if (acceptDeepestMatch) + { + return rootContainer ?? this; + } + else + { + throw; + } + } } public IContainer? GetParent() => _parent; diff --git a/src/Providers/FileTime.Providers.Smb/SmbContentReader.cs b/src/Providers/FileTime.Providers.Smb/SmbContentReader.cs index f1f39e5..72db473 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbContentReader.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbContentReader.cs @@ -9,7 +9,7 @@ namespace FileTime.Providers.Smb private readonly ISMBFileStore _smbFileStore; private readonly object _fileHandle; private readonly ISMBClient _client; - private bool disposed; + private bool _disposed; private long _bytesRead; public int PreferredBufferSize => (int)_client.MaxReadSize; @@ -53,7 +53,7 @@ namespace FileTime.Providers.Smb private void Dispose(bool disposing) { - if (!disposed) + if (!_disposed) { if (disposing) { @@ -61,7 +61,7 @@ namespace FileTime.Providers.Smb _smbFileStore.Disconnect(); } } - disposed = true; + _disposed = true; } } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Smb/SmbContentWriter.cs b/src/Providers/FileTime.Providers.Smb/SmbContentWriter.cs index 490fd21..063f5f0 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbContentWriter.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbContentWriter.cs @@ -1,24 +1,65 @@ using FileTime.Core.Providers; +using SMBLibrary; +using SMBLibrary.Client; namespace FileTime.Providers.Smb { public class SmbContentWriter : IContentWriter { - public int PreferredBufferSize => throw new NotImplementedException(); + private readonly ISMBFileStore _smbFileStore; + private readonly object _fileHandle; + private readonly ISMBClient _client; + private bool _disposed; + private int _writeOffset; - public void Dispose() + public int PreferredBufferSize => (int)_client.MaxWriteSize; + + public SmbContentWriter(ISMBFileStore smbFileStore, object fileHandle, ISMBClient client) { - throw new NotImplementedException(); + _smbFileStore = smbFileStore; + _fileHandle = fileHandle; + _client = client; } public Task FlushAsync() { - throw new NotImplementedException(); + return Task.CompletedTask; } public Task WriteBytesAsync(byte[] data) { - throw new NotImplementedException(); + var status = _smbFileStore.WriteFile(out int numberOfBytesWritten, _fileHandle, _writeOffset, data); + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception("Failed to write to file"); + } + _writeOffset += numberOfBytesWritten; + + return Task.CompletedTask; + } + + ~SmbContentWriter() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _smbFileStore.CloseFile(_fileHandle); + _smbFileStore.Disconnect(); + } + } + _disposed = true; } } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Smb/SmbFile.cs b/src/Providers/FileTime.Providers.Smb/SmbFile.cs index e8944f7..e56d3b9 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbFile.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbFile.cs @@ -37,9 +37,34 @@ namespace FileTime.Providers.Smb _smbShare = smbShare; } - public Task Delete(bool hardDelete = false) + public async Task Delete(bool hardDelete = false) { - throw new NotImplementedException(); + await _smbClientContext.RunWithSmbClientAsync(client => + { + var fileStore = _smbShare.TreeConnect(client, out var status); + status = fileStore.CreateFile( + out object fileHandle, + out FileStatus fileStatus, + GetPathFromShare(), + AccessMask.GENERIC_WRITE | AccessMask.DELETE | AccessMask.SYNCHRONIZE, + SMBLibrary.FileAttributes.Normal, + ShareAccess.None, + CreateDisposition.FILE_OPEN, + CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT, + null); + + if (status == NTStatus.STATUS_SUCCESS) + { + var fileDispositionInformation = new FileDispositionInformation + { + DeletePending = true + }; + status = fileStore.SetFileInformation(fileHandle, fileDispositionInformation); + bool deleteSucceeded = status == NTStatus.STATUS_SUCCESS; + status = fileStore.CloseFile(fileHandle); + } + status = fileStore.Disconnect(); + }); } public Task Rename(string newName) { @@ -69,9 +94,16 @@ namespace FileTime.Providers.Smb throw new Exception($"Could not open file {NativePath} for read."); } - var path = NativePath!; - path = path[(_parent.NativePath!.Length + 1)..]; - status = fileStore.CreateFile(out object fileHandle, out FileStatus fileStatus, path, AccessMask.GENERIC_READ | AccessMask.SYNCHRONIZE, SMBLibrary.FileAttributes.Normal, ShareAccess.Read, CreateDisposition.FILE_OPEN, CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT, null); + status = fileStore.CreateFile( + out object fileHandle, + out FileStatus fileStatus, + GetPathFromShare(), + AccessMask.GENERIC_READ | AccessMask.SYNCHRONIZE, + SMBLibrary.FileAttributes.Normal, + ShareAccess.Read, + CreateDisposition.FILE_OPEN, + CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT, + null); if (status != NTStatus.STATUS_SUCCESS) { @@ -82,9 +114,38 @@ namespace FileTime.Providers.Smb }); } - public Task GetContentWriterAsync() + public async Task GetContentWriterAsync() { - throw new NotImplementedException(); + return await _smbClientContext.RunWithSmbClientAsync(client => + { + NTStatus status = NTStatus.STATUS_DATA_ERROR; + var fileStore = _smbShare.TreeConnect(client, out status); + + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception($"Could not open file {NativePath} for write."); + } + + status = fileStore.CreateFile( + out object fileHandle, + out FileStatus fileStatus, + GetPathFromShare(), + AccessMask.GENERIC_WRITE | AccessMask.SYNCHRONIZE, + SMBLibrary.FileAttributes.Normal, + ShareAccess.None, + CreateDisposition.FILE_OPEN_IF, + CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT, + null); + + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception($"Could not open file {NativePath} for write."); + } + + return new SmbContentWriter(fileStore, fileHandle, client); + }); } + + private string GetPathFromShare() => SmbContentProvider.GetNativePath(FullName![(_smbShare.FullName!.Length + 1)..]); } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Smb/SmbFolder.cs b/src/Providers/FileTime.Providers.Smb/SmbFolder.cs index 603f9c8..51482a5 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbFolder.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbFolder.cs @@ -1,6 +1,7 @@ using AsyncEvent; using FileTime.Core.Models; using FileTime.Core.Providers; +using SMBLibrary; namespace FileTime.Providers.Smb { @@ -10,6 +11,7 @@ namespace FileTime.Providers.Smb private IReadOnlyList? _containers; private IReadOnlyList? _elements; private readonly IContainer? _parent; + private readonly SmbClientContext _smbClientContext; public string Name { get; } @@ -32,7 +34,7 @@ namespace FileTime.Providers.Smb public bool SupportsDirectoryLevelSoftDelete => false; - public SmbFolder(string name, SmbContentProvider contentProvider, SmbShare smbShare, IContainer parent) + public SmbFolder(string name, SmbContentProvider contentProvider, SmbShare smbShare, IContainer parent, SmbClientContext smbClientContext) { _parent = parent; SmbShare = smbShare; @@ -41,30 +43,65 @@ namespace FileTime.Providers.Smb FullName = parent?.FullName == null ? Name : parent.FullName + Constants.SeparatorChar + Name; NativePath = SmbContentProvider.GetNativePath(FullName); Provider = contentProvider; + _smbClientContext = smbClientContext; } - public Task CreateContainerAsync(string name) + public async Task CreateContainerAsync(string name) { - throw new NotImplementedException(); + var path = FullName![(SmbShare.FullName!.Length + 1)..] + Constants.SeparatorChar + name; + await SmbShare.CreateContainerWithPathAsync(SmbContentProvider.GetNativePath(path)); + await RefreshAsync(); + + return _containers!.FirstOrDefault(e => e.Name == name)!; } - public Task CreateElementAsync(string name) + public async Task CreateElementAsync(string name) { - throw new NotImplementedException(); + var path = FullName![(SmbShare.FullName!.Length + 1)..] + Constants.SeparatorChar + name; + await SmbShare.CreateElementWithPathAsync(SmbContentProvider.GetNativePath(path)); + await RefreshAsync(); + + return _elements!.FirstOrDefault(e => e.Name == name)!; } public Task CloneAsync() => Task.FromResult((IContainer)this); public IContainer? GetParent() => _parent; - public Task IsExistsAsync(string name) + public async Task IsExistsAsync(string name) { - throw new NotImplementedException(); + var items = await GetItems(); + return items?.Any(i => i.Name == name) ?? false; } - public Task Delete(bool hardDelete = false) + public async Task Delete(bool hardDelete = false) { - throw new NotImplementedException(); + await _smbClientContext.RunWithSmbClientAsync(client => + { + var fileStore = SmbShare.TreeConnect(client, out var status); + status = fileStore.CreateFile( + out object fileHandle, + out FileStatus fileStatus, + GetPathFromShare(), + AccessMask.GENERIC_WRITE | AccessMask.DELETE | AccessMask.SYNCHRONIZE, + SMBLibrary.FileAttributes.Normal, + ShareAccess.None, + CreateDisposition.FILE_OPEN, + CreateOptions.FILE_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT, + null); + + if (status == NTStatus.STATUS_SUCCESS) + { + var fileDispositionInformation = new FileDispositionInformation + { + DeletePending = true + }; + status = fileStore.SetFileInformation(fileHandle, fileDispositionInformation); + bool deleteSucceeded = status == NTStatus.STATUS_SUCCESS; + status = fileStore.CloseFile(fileHandle); + } + status = fileStore.Disconnect(); + }); } public Task Rename(string newName) { @@ -123,5 +160,7 @@ namespace FileTime.Providers.Smb _containers = null; _elements = null; } + + private string GetPathFromShare() => SmbContentProvider.GetNativePath(FullName![(SmbShare.FullName!.Length + 1)..]); } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Smb/SmbServer.cs b/src/Providers/FileTime.Providers.Smb/SmbServer.cs index eb69f9e..b9a94ca 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbServer.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbServer.cs @@ -10,6 +10,8 @@ namespace FileTime.Providers.Smb { public class SmbServer : IContainer { + internal const int MAXRETRIES = 5; + private bool _reenterCredentials; private IReadOnlyList? _shares; @@ -20,6 +22,8 @@ namespace FileTime.Providers.Smb private bool _refreshingClient; private readonly IInputInterface _inputInterface; private readonly SmbClientContext _smbClientContext; + private readonly List _exceptions = new(); + public string? Username { get; private set; } public string? Password { get; private set; } @@ -35,7 +39,7 @@ namespace FileTime.Providers.Smb IContentProvider IItem.Provider => Provider; public SupportsDelete CanDelete => SupportsDelete.True; public bool CanRename => false; - public IReadOnlyList Exceptions { get; } = new List().AsReadOnly(); + public IReadOnlyList Exceptions { get; } public AsyncEventHandler Refreshed { get; } = new(); @@ -47,6 +51,7 @@ namespace FileTime.Providers.Smb { _inputInterface = inputInterface; _smbClientContext = new SmbClientContext(GetSmbClient, DisposeSmbClient); + Exceptions = _exceptions.AsReadOnly(); Username = username; Password = password; @@ -113,11 +118,19 @@ namespace FileTime.Providers.Smb public async Task RefreshAsync(CancellationToken token = default) { - List shares = await _smbClientContext.RunWithSmbClientAsync((client) => client.ListShares(out var status)); + try + { + _exceptions.Clear(); + List shares = await _smbClientContext.RunWithSmbClientAsync((client) => client.ListShares(out var status), _shares == null ? 1 : MAXRETRIES); - _shares = shares.ConvertAll(s => new SmbShare(s, Provider, this, _smbClientContext)).AsReadOnly(); - _items = _shares.Cast().ToList().AsReadOnly(); - await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty, token); + _shares = shares.ConvertAll(s => new SmbShare(s, Provider, this, _smbClientContext)).AsReadOnly(); + _items = _shares.Cast().ToList().AsReadOnly(); + await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty, token); + } + catch (Exception e) + { + _exceptions.Add(e); + } } public Task CloneAsync() => Task.FromResult((IContainer)this); @@ -130,7 +143,7 @@ namespace FileTime.Providers.Smb } } - private async Task GetSmbClient() + private async Task GetSmbClient(int maxRetries = MAXRETRIES) { bool isClientNull; lock (_clientGuard) @@ -138,6 +151,7 @@ namespace FileTime.Providers.Smb isClientNull = _client == null; } + int reTries = 0; while (isClientNull) { if (!await RefreshSmbClient()) @@ -149,6 +163,12 @@ namespace FileTime.Providers.Smb { isClientNull = _client == null; } + + if (reTries >= maxRetries) + { + throw new Exception($"Could not connect to server {Name} after {reTries} retry"); + } + reTries++; } return _client!; } diff --git a/src/Providers/FileTime.Providers.Smb/SmbShare.cs b/src/Providers/FileTime.Providers.Smb/SmbShare.cs index 6875f0d..9f14f91 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbShare.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbShare.cs @@ -61,28 +61,99 @@ namespace FileTime.Providers.Smb return _elements; } - public Task CreateContainerAsync(string name) + public async Task CreateContainerAsync(string name) { - throw new NotImplementedException(); + await CreateContainerWithPathAsync(name); + await RefreshAsync(); + + return _containers!.FirstOrDefault(e => e.Name == name)!; + } + internal async Task CreateContainerWithPathAsync(string path) + { + await _smbClientContext.RunWithSmbClientAsync(client => + { + NTStatus status = NTStatus.STATUS_DATA_ERROR; + var fileStore = TreeConnect(client, out status); + + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception($"Could not create directory {path}."); + } + + status = fileStore.CreateFile( + out object fileHandle, + out FileStatus fileStatus, + path, + AccessMask.GENERIC_ALL, + SMBLibrary.FileAttributes.Directory, + ShareAccess.Read, + CreateDisposition.FILE_OPEN_IF, + CreateOptions.FILE_DIRECTORY_FILE, + null); + + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception($"Could not create directory {path}."); + } + + fileStore.CloseFile(fileHandle); + fileStore.Disconnect(); + }); } - public Task CreateElementAsync(string name) + public async Task CreateElementAsync(string name) { - throw new NotImplementedException(); + await CreateElementWithPathAsync(name); + await RefreshAsync(); + + return _elements!.FirstOrDefault(e => e.Name == name)!; + } + internal async Task CreateElementWithPathAsync(string path) + { + await _smbClientContext.RunWithSmbClientAsync(client => + { + NTStatus status = NTStatus.STATUS_DATA_ERROR; + var fileStore = TreeConnect(client, out status); + + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception($"Could not create file {path}."); + } + + status = fileStore.CreateFile( + out object fileHandle, + out FileStatus fileStatus, + path, + AccessMask.GENERIC_WRITE | AccessMask.SYNCHRONIZE, + SMBLibrary.FileAttributes.Normal, + ShareAccess.None, + CreateDisposition.FILE_CREATE, + CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT, + null); + + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception($"Could not create file {path}."); + } + + fileStore.CloseFile(fileHandle); + fileStore.Disconnect(); + }); } public Task Delete(bool hardDelete = false) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public IContainer? GetParent() => _parent; public Task CloneAsync() => Task.FromResult((IContainer)this); - public Task IsExistsAsync(string name) + public async Task IsExistsAsync(string name) { - throw new NotImplementedException(); + var items = await GetItems(); + return items?.Any(i => i.Name == name) ?? false; } public async Task RefreshAsync(CancellationToken token = default) @@ -134,7 +205,7 @@ namespace FileTime.Providers.Smb { if ((fileDirectoryInformation.FileAttributes & SMBLibrary.FileAttributes.Directory) == SMBLibrary.FileAttributes.Directory) { - containers.Add(new SmbFolder(fileDirectoryInformation.FileName, Provider, this, parent)); + containers.Add(new SmbFolder(fileDirectoryInformation.FileName, Provider, this, parent, _smbClientContext)); } else {