SMB, GUI imput handler, ThreadSafe SMB
This commit is contained in:
@@ -54,7 +54,10 @@ namespace FileTime.Core.Components
|
||||
IItem? itemToSelect = null;
|
||||
if (value != null)
|
||||
{
|
||||
itemToSelect = (await _currentLocation.GetItems())?.FirstOrDefault(i => i.FullName == value?.FullName);
|
||||
itemToSelect = (await _currentLocation.GetItems())?.FirstOrDefault(i =>
|
||||
i.FullName == null && value?.FullName == null
|
||||
? i.Name == value?.Name
|
||||
: i.FullName == value?.FullName);
|
||||
if (itemToSelect == null) throw new IndexOutOfRangeException("Provided item does not exists in the current container.");
|
||||
}
|
||||
|
||||
@@ -123,7 +126,7 @@ namespace FileTime.Core.Components
|
||||
|
||||
private async Task HandleCurrentLocationRefresh(object? sender, AsyncEventArgs e)
|
||||
{
|
||||
var currentSelectedName = (await GetCurrentSelectedItem())?.FullName ?? (await GetItemByLastPath()).FullName;
|
||||
var currentSelectedName = (await GetCurrentSelectedItem())?.FullName ?? (await GetItemByLastPath())?.FullName;
|
||||
var currentLocationItems = (await (await GetCurrentLocation()).GetItems())!;
|
||||
if (currentSelectedName != null)
|
||||
{
|
||||
|
||||
9
src/Core/FileTime.Core/Interactions/BasicInputHandler.cs
Normal file
9
src/Core/FileTime.Core/Interactions/BasicInputHandler.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace FileTime.Core.Interactions
|
||||
{
|
||||
public class BasicInputHandler : IInputInterface
|
||||
{
|
||||
public Func<IEnumerable<InputElement>, Task<string?[]>>? InputHandler { get; set; }
|
||||
public async Task<string?[]> ReadInputs(IEnumerable<InputElement> fields) =>
|
||||
InputHandler != null ? await InputHandler.Invoke(fields) : throw new NotImplementedException(nameof(InputHandler) + " is not set");
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using FileTime.Avalonia.Application;
|
||||
using FileTime.Avalonia.Services;
|
||||
using FileTime.Avalonia.ViewModels;
|
||||
using FileTime.Core.Command;
|
||||
using FileTime.Core.Interactions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FileTime.Avalonia
|
||||
@@ -13,7 +14,8 @@ namespace FileTime.Avalonia
|
||||
{
|
||||
return serviceCollection
|
||||
.AddSingleton<AppState>()
|
||||
.AddTransient<MainPageViewModel>();
|
||||
.AddTransient<MainPageViewModel>()
|
||||
.AddSingleton<IInputInterface, BasicInputHandler>();
|
||||
}
|
||||
internal static IServiceCollection AddServices(this IServiceCollection serviceCollection)
|
||||
{
|
||||
|
||||
@@ -15,8 +15,9 @@ namespace FileTime.Avalonia.ViewModels
|
||||
{
|
||||
[ViewModel]
|
||||
[Inject(typeof(ItemNameConverterService))]
|
||||
public partial class ContainerViewModel : IItemViewModel
|
||||
public partial class ContainerViewModel : IItemViewModel, IDisposable
|
||||
{
|
||||
private bool _disposed;
|
||||
private bool _isRefreshing;
|
||||
private bool _isInitialized;
|
||||
private INewItemProcessor _newItemProcessor;
|
||||
@@ -128,13 +129,20 @@ namespace FileTime.Avalonia.ViewModels
|
||||
{
|
||||
_isRefreshing = true;
|
||||
|
||||
var containers = (await _container.GetContainers()).Select(c => AdoptOrCreateItem(c, (c2) => new ContainerViewModel(_newItemProcessor, this, c2, ItemNameConverterService))).ToList();
|
||||
var elements = (await _container.GetElements()).Select(e => AdoptOrCreateItem(e, (e2) => new ElementViewModel(e2, this, ItemNameConverterService))).ToList();
|
||||
var containers = (await _container.GetContainers())!.Select(c => AdoptOrReuseOrCreateItem(c, (c2) => new ContainerViewModel(_newItemProcessor, this, c2, ItemNameConverterService))).ToList();
|
||||
var elements = (await _container.GetElements())!.Select(e => AdoptOrReuseOrCreateItem(e, (e2) => new ElementViewModel(e2, this, ItemNameConverterService))).ToList();
|
||||
|
||||
var containersToRemove = _containers.Except(containers);
|
||||
|
||||
_containers.Clear();
|
||||
_elements.Clear();
|
||||
_items.Clear();
|
||||
|
||||
foreach (var containerToRemove in containersToRemove)
|
||||
{
|
||||
containerToRemove?.Dispose();
|
||||
}
|
||||
|
||||
foreach (var container in containers)
|
||||
{
|
||||
if (initializeChildren) await container.Init(false);
|
||||
@@ -161,11 +169,14 @@ namespace FileTime.Avalonia.ViewModels
|
||||
_isRefreshing = false;
|
||||
}
|
||||
|
||||
private TResult AdoptOrCreateItem<T, TResult>(T item, Func<T, TResult> generator) where T : IItem
|
||||
private TResult AdoptOrReuseOrCreateItem<T, TResult>(T item, Func<T, TResult> generator) where T : class, IItem
|
||||
{
|
||||
var itemToAdopt = ChildrenToAdopt.Find(i => i.Item.Name == item.Name);
|
||||
var itemToAdopt = ChildrenToAdopt.Find(i => i.Item == item);
|
||||
if (itemToAdopt is TResult itemViewModel) return itemViewModel;
|
||||
|
||||
var existingViewModel = _items?.FirstOrDefault(i => i.Item == item);
|
||||
if (existingViewModel is TResult itemViewModelToReuse) return itemViewModelToReuse;
|
||||
|
||||
return generator(item);
|
||||
}
|
||||
|
||||
@@ -177,6 +188,7 @@ namespace FileTime.Avalonia.ViewModels
|
||||
foreach (var container in _containers)
|
||||
{
|
||||
container.Unload(true);
|
||||
container.Dispose();
|
||||
container.ChildrenToAdopt.Clear();
|
||||
}
|
||||
}
|
||||
@@ -203,5 +215,25 @@ namespace FileTime.Avalonia.ViewModels
|
||||
if (!_isInitialized) await Task.Run(Refresh);
|
||||
return _items;
|
||||
}
|
||||
|
||||
~ContainerViewModel()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
Container.Refreshed.Remove(Container_Refreshed);
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ using FileTime.App.Core.Clipboard;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using FileTime.Core.Command;
|
||||
using FileTime.Core.Timeline;
|
||||
using FileTime.Core.Providers;
|
||||
|
||||
namespace FileTime.Avalonia.ViewModels
|
||||
{
|
||||
@@ -40,7 +41,7 @@ namespace FileTime.Avalonia.ViewModels
|
||||
|
||||
private IClipboard _clipboard;
|
||||
private TimeRunner _timeRunner;
|
||||
|
||||
private IEnumerable<IContentProvider>? _contentProviders;
|
||||
private Action? _inputHandler;
|
||||
|
||||
[Property]
|
||||
@@ -67,6 +68,11 @@ namespace FileTime.Avalonia.ViewModels
|
||||
{
|
||||
_clipboard = App.ServiceProvider.GetService<IClipboard>()!;
|
||||
_timeRunner = App.ServiceProvider.GetService<TimeRunner>()!;
|
||||
_contentProviders = App.ServiceProvider.GetService<IEnumerable<IContentProvider>>();
|
||||
var inputInterface = (BasicInputHandler)App.ServiceProvider.GetService<IInputInterface>()!;
|
||||
inputInterface.InputHandler = ReadInputs2;
|
||||
App.ServiceProvider.GetService<TopContainer>();
|
||||
|
||||
_timeRunner.CommandsChanged += (o, e) => OnPropertyChanged(nameof(TimelineCommands));
|
||||
InitCommandBindings();
|
||||
|
||||
@@ -496,6 +502,19 @@ namespace FileTime.Avalonia.ViewModels
|
||||
await _timeRunner.Refresh();
|
||||
}
|
||||
|
||||
private async Task GoToContainer()
|
||||
{
|
||||
var handler = () =>
|
||||
{
|
||||
if (Inputs != null)
|
||||
{
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
ReadInputs(new List<Core.Interactions.InputElement>() { new Core.Interactions.InputElement("Path", InputType.Text) }, handler);
|
||||
}
|
||||
|
||||
[Command]
|
||||
public void ProcessInputs()
|
||||
{
|
||||
@@ -669,6 +688,24 @@ namespace FileTime.Avalonia.ViewModels
|
||||
_inputHandler = inputHandler;
|
||||
}
|
||||
|
||||
public async Task<string?[]> ReadInputs2(IEnumerable<Core.Interactions.InputElement> fields)
|
||||
{
|
||||
var waiting = true;
|
||||
var result = new string[0];
|
||||
ReadInputs(fields.ToList(), () =>
|
||||
{
|
||||
if(Inputs != null)
|
||||
{
|
||||
result = Inputs.Select(i => i.Value).ToArray();
|
||||
}
|
||||
waiting = false;
|
||||
});
|
||||
|
||||
while (waiting) await Task.Delay(100);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ShowMessageBox(string text, Action inputHandler)
|
||||
{
|
||||
MessageBoxText = text;
|
||||
@@ -847,6 +884,11 @@ namespace FileTime.Avalonia.ViewModels
|
||||
FileTime.App.Core.Command.Commands.Refresh,
|
||||
new KeyWithModifiers[]{new KeyWithModifiers(Key.R)},
|
||||
RefreshCurrentLocation),
|
||||
new CommandBinding(
|
||||
"go to",
|
||||
FileTime.App.Core.Command.Commands.Refresh,
|
||||
new KeyWithModifiers[]{new KeyWithModifiers(Key.L, ctrl: true)},
|
||||
GoToContainer),
|
||||
};
|
||||
var universalCommandBindings = new List<CommandBinding>()
|
||||
{
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace FileTime.Providers.Local
|
||||
{
|
||||
var pathParts = (IsCaseInsensitive ? path.ToLower() : path).TrimStart(Constants.SeparatorChar).Split(Constants.SeparatorChar);
|
||||
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && pathParts.Length == 1 && pathParts[0] == "") return this;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && pathParts.Length == 1 && pathParts[0] == "") return this;
|
||||
|
||||
var rootContainer = _rootContainers.FirstOrDefault(c => NormalizePath(c.Name) == NormalizePath(pathParts[0]));
|
||||
|
||||
|
||||
62
src/Providers/FileTime.Providers.Smb/SmbClientContext.cs
Normal file
62
src/Providers/FileTime.Providers.Smb/SmbClientContext.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using SMBLibrary.Client;
|
||||
|
||||
namespace FileTime.Providers.Smb
|
||||
{
|
||||
public class SmbClientContext
|
||||
{
|
||||
private readonly Func<Task<ISMBClient>> _getSmbClient;
|
||||
private readonly Action _disposeClient;
|
||||
private bool _isRunning;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
public SmbClientContext(Func<Task<ISMBClient>> getSmbClient, Action disposeClient)
|
||||
{
|
||||
_getSmbClient = getSmbClient;
|
||||
_disposeClient = disposeClient;
|
||||
}
|
||||
|
||||
public async Task<T> RunWithSmbClientAsync<T>(Func<ISMBClient, T> func)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if(!_isRunning)
|
||||
{
|
||||
_isRunning = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(1);
|
||||
}
|
||||
try
|
||||
{
|
||||
ISMBClient client;
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
client = await _getSmbClient();
|
||||
return func(client);
|
||||
}
|
||||
catch (Exception e) when (e.Source == "SMBLibrary")
|
||||
{
|
||||
_disposeClient();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new Exception("Exception was thrown while executing method with SmbClient.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_isRunning = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,9 +63,21 @@ namespace FileTime.Providers.Smb
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public Task<IItem?> GetByPath(string path)
|
||||
public async Task<IItem?> GetByPath(string path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
if (path == null) return this;
|
||||
|
||||
var pathParts = path.TrimStart(Constants.SeparatorChar).Split(Constants.SeparatorChar);
|
||||
|
||||
var rootContainer = _rootContainers.Find(c => c.Name == pathParts[0]);
|
||||
|
||||
if (rootContainer == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var remainingPath = string.Join(Constants.SeparatorChar, pathParts.Skip(1));
|
||||
return remainingPath.Length == 0 ? rootContainer : await rootContainer.GetByPath(remainingPath);
|
||||
}
|
||||
|
||||
public IContainer? GetParent() => _parent;
|
||||
|
||||
@@ -17,7 +17,10 @@ namespace FileTime.Providers.Smb
|
||||
private IReadOnlyList<IItem>? _items;
|
||||
private readonly IReadOnlyList<IElement>? _elements = new List<IElement>().AsReadOnly();
|
||||
private ISMBClient? _client;
|
||||
private readonly object _clientGuard = new object();
|
||||
private bool _refreshingClient;
|
||||
private readonly IInputInterface _inputInterface;
|
||||
private readonly SmbClientContext _smbClientContext;
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
@@ -37,6 +40,7 @@ namespace FileTime.Providers.Smb
|
||||
public SmbServer(string path, SmbContentProvider contentProvider, IInputInterface inputInterface)
|
||||
{
|
||||
_inputInterface = inputInterface;
|
||||
_smbClientContext = new SmbClientContext(GetSmbClient, DisposeSmbClient);
|
||||
|
||||
Provider = contentProvider;
|
||||
FullName = Name = path;
|
||||
@@ -86,26 +90,60 @@ namespace FileTime.Providers.Smb
|
||||
|
||||
public async Task Refresh()
|
||||
{
|
||||
ISMBClient client = await GetSmbClient();
|
||||
List<string> shares = await _smbClientContext.RunWithSmbClientAsync((client) => client.ListShares(out var status));
|
||||
|
||||
List<string> shares = client.ListShares(out var status);
|
||||
|
||||
_shares = shares.ConvertAll(s => new SmbShare(s, Provider, this, GetSmbClient)).AsReadOnly();
|
||||
_shares = shares.ConvertAll(s => new SmbShare(s, Provider, this, _smbClientContext)).AsReadOnly();
|
||||
_items = _shares.Cast<IItem>().ToList().AsReadOnly();
|
||||
await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty);
|
||||
}
|
||||
|
||||
public Task<IContainer> Clone() => Task.FromResult((IContainer)this);
|
||||
|
||||
private void DisposeSmbClient()
|
||||
{
|
||||
lock (_clientGuard)
|
||||
{
|
||||
_client = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ISMBClient> GetSmbClient()
|
||||
{
|
||||
if (_client == null)
|
||||
bool isClientNull;
|
||||
lock (_clientGuard)
|
||||
{
|
||||
isClientNull = _client == null;
|
||||
}
|
||||
|
||||
while (isClientNull)
|
||||
{
|
||||
if (!await RefreshSmbClient())
|
||||
{
|
||||
await Task.Delay(1);
|
||||
}
|
||||
|
||||
lock (_clientGuard)
|
||||
{
|
||||
isClientNull = _client == null;
|
||||
}
|
||||
}
|
||||
return _client!;
|
||||
}
|
||||
|
||||
private async Task<bool> RefreshSmbClient()
|
||||
{
|
||||
lock (_clientGuard)
|
||||
{
|
||||
if (_refreshingClient) return false;
|
||||
_refreshingClient = true;
|
||||
}
|
||||
try
|
||||
{
|
||||
var couldParse = IPAddress.TryParse(Name[2..], out var ipAddress);
|
||||
_client = new SMB2Client();
|
||||
var client = new SMB2Client();
|
||||
var connected = couldParse
|
||||
? _client.Connect(ipAddress, SMBTransportType.DirectTCPTransport)
|
||||
: _client.Connect(Name[2..], SMBTransportType.DirectTCPTransport);
|
||||
? client.Connect(ipAddress, SMBTransportType.DirectTCPTransport)
|
||||
: client.Connect(Name[2..], SMBTransportType.DirectTCPTransport);
|
||||
|
||||
if (connected)
|
||||
{
|
||||
@@ -122,14 +160,29 @@ namespace FileTime.Providers.Smb
|
||||
_password = inputs[1];
|
||||
}
|
||||
|
||||
if (_client.Login(string.Empty, _username, _password) != NTStatus.STATUS_SUCCESS)
|
||||
if (client.Login(string.Empty, _username, _password) != NTStatus.STATUS_SUCCESS)
|
||||
{
|
||||
_username = null;
|
||||
_password = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_clientGuard)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return _client;
|
||||
finally
|
||||
{
|
||||
lock (_clientGuard)
|
||||
{
|
||||
_refreshingClient = false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task Rename(string newName) => throw new NotSupportedException();
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace FileTime.Providers.Smb
|
||||
private IReadOnlyList<IItem>? _items;
|
||||
private IReadOnlyList<IContainer>? _containers;
|
||||
private IReadOnlyList<IElement>? _elements;
|
||||
private Func<Task<ISMBClient>> _getSmbClient;
|
||||
private SmbClientContext _smbClientContext;
|
||||
private readonly IContainer? _parent;
|
||||
|
||||
public string Name { get; }
|
||||
@@ -28,10 +28,10 @@ namespace FileTime.Providers.Smb
|
||||
|
||||
public AsyncEventHandler Refreshed { get; } = new();
|
||||
|
||||
public SmbShare(string name, SmbContentProvider contentProvider, IContainer parent, Func<Task<ISMBClient>> getSmbClient)
|
||||
public SmbShare(string name, SmbContentProvider contentProvider, IContainer parent, SmbClientContext smbClientContext)
|
||||
{
|
||||
_parent = parent;
|
||||
_getSmbClient = getSmbClient;
|
||||
_smbClientContext = smbClientContext;
|
||||
|
||||
Name = name;
|
||||
FullName = parent?.FullName == null ? Name : parent.FullName + Constants.SeparatorChar + Name;
|
||||
@@ -117,40 +117,42 @@ namespace FileTime.Providers.Smb
|
||||
|
||||
public async Task<(List<IContainer> containers, List<IElement> elements)> ListFolder(IContainer parent, string shareName, string folderName)
|
||||
{
|
||||
var containers = new List<IContainer>();
|
||||
var elements = new List<IElement>();
|
||||
|
||||
var client = await _getSmbClient();
|
||||
ISMBFileStore fileStore = client.TreeConnect(shareName, out var status);
|
||||
if (status == NTStatus.STATUS_SUCCESS)
|
||||
return await _smbClientContext.RunWithSmbClientAsync(client =>
|
||||
{
|
||||
status = fileStore.CreateFile(out object directoryHandle, out FileStatus fileStatus, folderName, AccessMask.GENERIC_READ, SMBLibrary.FileAttributes.Directory, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null);
|
||||
var containers = new List<IContainer>();
|
||||
var elements = new List<IElement>();
|
||||
NTStatus status = NTStatus.STATUS_DATA_ERROR;
|
||||
ISMBFileStore fileStore = client.TreeConnect(shareName, out status);
|
||||
if (status == NTStatus.STATUS_SUCCESS)
|
||||
{
|
||||
status = fileStore.QueryDirectory(out List<QueryDirectoryFileInformation> fileList, directoryHandle, "*", FileInformationClass.FileDirectoryInformation);
|
||||
status = fileStore.CloseFile(directoryHandle);
|
||||
|
||||
foreach (var item in fileList)
|
||||
status = fileStore.CreateFile(out object directoryHandle, out FileStatus fileStatus, folderName, AccessMask.GENERIC_READ, SMBLibrary.FileAttributes.Directory, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null);
|
||||
if (status == NTStatus.STATUS_SUCCESS)
|
||||
{
|
||||
if (item is FileDirectoryInformation fileDirectoryInformation && fileDirectoryInformation.FileName != "." && fileDirectoryInformation.FileName != "..")
|
||||
status = fileStore.QueryDirectory(out List<QueryDirectoryFileInformation> fileList, directoryHandle, "*", FileInformationClass.FileDirectoryInformation);
|
||||
status = fileStore.CloseFile(directoryHandle);
|
||||
|
||||
foreach (var item in fileList)
|
||||
{
|
||||
if ((fileDirectoryInformation.FileAttributes & SMBLibrary.FileAttributes.Directory) == SMBLibrary.FileAttributes.Directory)
|
||||
if (item is FileDirectoryInformation fileDirectoryInformation && fileDirectoryInformation.FileName != "." && fileDirectoryInformation.FileName != "..")
|
||||
{
|
||||
containers.Add(new SmbFolder(fileDirectoryInformation.FileName, Provider, this, parent));
|
||||
}
|
||||
else
|
||||
{
|
||||
elements.Add(new SmbFile(fileDirectoryInformation.FileName, Provider, parent));
|
||||
if ((fileDirectoryInformation.FileAttributes & SMBLibrary.FileAttributes.Directory) == SMBLibrary.FileAttributes.Directory)
|
||||
{
|
||||
containers.Add(new SmbFolder(fileDirectoryInformation.FileName, Provider, this, parent));
|
||||
}
|
||||
else
|
||||
{
|
||||
elements.Add(new SmbFile(fileDirectoryInformation.FileName, Provider, parent));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
containers = containers.OrderBy(c => c.Name).ToList();
|
||||
elements = elements.OrderBy(e => e.Name).ToList();
|
||||
containers = containers.OrderBy(c => c.Name).ToList();
|
||||
elements = elements.OrderBy(e => e.Name).ToList();
|
||||
|
||||
return (containers, elements);
|
||||
return (containers, elements);
|
||||
});
|
||||
}
|
||||
|
||||
public Task Rename(string newName) => throw new NotSupportedException();
|
||||
|
||||
Reference in New Issue
Block a user