mirror of
https://gitlab.com/freepascal.org/fpc/pas2js.git
synced 2025-04-08 08:07:49 +02:00
* Add FindFirst/FindNext support
This commit is contained in:
parent
72b7d29497
commit
5d3abdcc5d
demo/wasienv/filesystem
packages
@ -43,14 +43,6 @@
|
||||
<Item0 Name="PasJSIsProjectHTMLFile" Value="1"/>
|
||||
</CustomData>
|
||||
</Unit>
|
||||
<Unit>
|
||||
<Filename Value="wasitypes.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
</Unit>
|
||||
<Unit>
|
||||
<Filename Value="wasizenfs.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
</Unit>
|
||||
</Units>
|
||||
</ProjectOptions>
|
||||
<CompilerOptions>
|
||||
|
@ -23,8 +23,8 @@ end;
|
||||
procedure TMyApplication.RunWasm;
|
||||
|
||||
begin
|
||||
// Writeln('Enabling logging');
|
||||
// WasiEnvironment.LogAPI:=True;
|
||||
// Writeln('Enabling logging');
|
||||
// WasiEnvironment.LogAPI:=True;
|
||||
await(tjsobject, ZenFS.configure(
|
||||
new(
|
||||
['mounts', new([
|
||||
@ -33,6 +33,10 @@ begin
|
||||
])
|
||||
)
|
||||
);
|
||||
if not ZenFS.existsSync('/tmp') then
|
||||
begin
|
||||
ZenFS.mkdirSync('/tmp',777);
|
||||
end;
|
||||
FS:=TWASIZenFS.Create;
|
||||
WasiEnvironment.FS:=FS;
|
||||
StartWebAssembly('fsdemo.wasm');
|
||||
|
@ -10,6 +10,34 @@ Const
|
||||
{$Endif}
|
||||
OurFile = OurDir+'/test.txt';
|
||||
|
||||
Procedure ShowDir(aDir : String);
|
||||
|
||||
var
|
||||
Info : TSearchRec;
|
||||
aFileCount : Integer;
|
||||
TotalSize : Int64;
|
||||
S : String;
|
||||
begin
|
||||
TotalSize:=0;
|
||||
aFileCount:=0;
|
||||
If FindFirst(aDir+'*',faAnyFile,Info)=0 then
|
||||
try
|
||||
Repeat
|
||||
S:=Info.Name;
|
||||
if (Info.Attr and faDirectory)<>0 then
|
||||
S:=S+'/';
|
||||
if (Info.Attr and faSymLink)<>0 then
|
||||
S:=S+'@';
|
||||
Writeln(FormatDateTime('yyyy-mm-dd"T"hh:nn:ss',Info.TimeStamp),' ',Info.Size:10,' '+S);
|
||||
TotalSize:=TotalSize+Info.Size;
|
||||
inc(aFileCount);
|
||||
until FindNext(Info)<>0;
|
||||
finally
|
||||
FindClose(Info)
|
||||
end;
|
||||
Writeln('Total: ',TotalSize,' bytes in ',aFileCount,' files');
|
||||
end;
|
||||
|
||||
var
|
||||
HasDir : Boolean;
|
||||
HasFile: Boolean;
|
||||
@ -17,7 +45,6 @@ var
|
||||
aSize,FD,byteCount : Integer;
|
||||
|
||||
begin
|
||||
S:='Hello, WebAssembly World!';
|
||||
HasDir:=DirectoryExists(OurDir);
|
||||
if HasDir then
|
||||
Writeln('Directory already exists: ',OurDir)
|
||||
@ -25,6 +52,8 @@ begin
|
||||
Writeln('Created new directory: ',OurDir)
|
||||
else
|
||||
Writeln('Failed to create directory: ',OurDir);
|
||||
Writeln('Contents of root:');
|
||||
ShowDir('/');
|
||||
HasFile:=FileExists(OurFile);
|
||||
If HasFile then
|
||||
Writeln('File exists: ',OurFile)
|
||||
@ -37,12 +66,15 @@ begin
|
||||
else
|
||||
begin
|
||||
Writeln('Got fileHandle: ',FD);
|
||||
S:='Hello, WebAssembly World!';
|
||||
ByteCount:=FileWrite(FD,S[1],Length(S));
|
||||
Writeln('Wrote ',byteCount,' bytes to file. Expected: ',Length(S));
|
||||
FileClose(FD);
|
||||
Writeln('Closed file');
|
||||
end;
|
||||
end;
|
||||
Writeln('Contents of ',OurDir,':');
|
||||
ShowDir(ourdir+'/');
|
||||
If FileExists(OurFile) then
|
||||
begin
|
||||
Writeln('Opening file: ',OurFile);
|
||||
|
@ -143,7 +143,7 @@ type
|
||||
function fd_prestat_get(fd: NativeInt; bufPtr: TWasmMemoryLocation) : NativeInt; virtual;
|
||||
function fd_pwrite(fd, iovs, iovsLen, offset, nwritten : NativeInt) : NativeInt;virtual;
|
||||
function fd_read(fd: NativeInt; iovs : TWasmMemoryLocation; iovsLen: NativeInt; nread : TWasmMemoryLocation) : NativeInt; virtual;
|
||||
function fd_readdir(fd, bufPtr, bufLen, cookie, bufusedPtr : NativeInt) : NativeInt; virtual;
|
||||
function fd_readdir(fd : NativeInt; bufPtr: TWasmMemoryLocation; bufLen, cookie: NativeInt; bufusedPtr : TWasmMemoryLocation) : NativeInt; virtual;
|
||||
function fd_renumber(afrom,ato : NativeInt) : NativeInt; virtual;
|
||||
function fd_seek(fd, offset, whence : NativeInt; newOffsetPtr : TWasmMemoryLocation) : NativeInt; virtual;
|
||||
function fd_sync(fd : NativeInt) : NativeInt; virtual;
|
||||
@ -1434,7 +1434,6 @@ begin
|
||||
if LogAPI then
|
||||
DoLog('TPas2JSWASIEnvironment.fd_seek(%d,%d,%d,[%x])',[fd,offset,whence,newOffsetPtr]);
|
||||
{$ENDIF}
|
||||
console.log('Unimplemented: TPas2JSWASIEnvironment.fd_seek');
|
||||
if not Assigned(FS) then
|
||||
Result:=WASI_ENOSYS
|
||||
else
|
||||
@ -1633,15 +1632,51 @@ begin
|
||||
Result:=TJSUint8Array.New(0);
|
||||
end;
|
||||
|
||||
function TPas2JSWASIEnvironment.fd_readdir(fd, bufPtr, bufLen, cookie,
|
||||
bufusedPtr: NativeInt): NativeInt;
|
||||
function TPas2JSWASIEnvironment.fd_readdir(fd: NativeInt; bufPtr: TWasmMemoryLocation; bufLen, cookie: NativeInt;
|
||||
bufusedPtr: TWasmMemoryLocation): NativeInt;
|
||||
|
||||
var
|
||||
Dirent : TWasiFSDirent;
|
||||
NameArray : TJSUint8Array;
|
||||
NameLen : integer;
|
||||
Ptr : TWasmMemoryLocation;
|
||||
Res,Used : Integer;
|
||||
|
||||
|
||||
begin
|
||||
{$IFNDEF NO_WASI_DEBUG}
|
||||
if LogAPI then
|
||||
DoLog('TPas2JSWASIEnvironment.fd_readdir(%d,[%x],%d,%d,[%x])',[fd,bufPtr,buflen,cookie,bufusedptr]);
|
||||
{$ENDIF}
|
||||
console.log('Unimplemented: TPas2JSWASIEnvironment.fd_readdir');
|
||||
Result:= WASI_ENOSYS;
|
||||
if not Assigned(FS) then
|
||||
Result:=WASI_ENOSYS
|
||||
else
|
||||
try
|
||||
Res:=FS.ReadDir(FD,AsIntNumber(Cookie),Dirent);
|
||||
Result:=WASI_ESUCCESS;
|
||||
Ptr:=BufPtr;
|
||||
While ((Ptr-BufPtr)<BufLen) and (Res=WASI_ESUCCESS) do
|
||||
begin
|
||||
NameArray:=UTF8TextEncoder.encode(Dirent.name);
|
||||
NameLen:=NameArray.byteLength;
|
||||
Ptr:=SetMemInfoUInt64(Ptr,Dirent.Next);
|
||||
Ptr:=SetMemInfoUInt64(Ptr,Dirent.ino);
|
||||
Ptr:=SetMemInfoInt32(Ptr,NameLen);
|
||||
Ptr:=SetMemInfoInt32(Ptr,DirentMap[Dirent.EntryType]);
|
||||
if SetUTF8StringInMem(Ptr,BufLen-18,Dirent.Name)<>-1 then
|
||||
begin
|
||||
Ptr:=Ptr+NameLen;
|
||||
Cookie:=Dirent.Next;
|
||||
Res:=FS.ReadDir(FD,AsIntNumber(Cookie),Dirent)
|
||||
end
|
||||
else
|
||||
Res:=WASI_ENOMEM;
|
||||
end;
|
||||
SetMemInfoInt32(bufusedPtr,Ptr-BufPtr);
|
||||
except
|
||||
On E : Exception do
|
||||
Result:=ErrorToCode(E);
|
||||
end;
|
||||
end;
|
||||
|
||||
function TPas2JSWASIEnvironment.fd_renumber(afrom, ato: NativeInt): NativeInt;
|
||||
@ -1811,12 +1846,12 @@ begin
|
||||
Loc:=BufPtr;
|
||||
Loc:=SetMemInfoInt64(Loc,Info.dev);
|
||||
Loc:=SetMemInfoUInt64(Loc,Info.Ino);
|
||||
Loc:=SetMemInfoInt8(Loc,Info.filetype);
|
||||
Loc:=SetMemInfoUInt64(Loc,Info.filetype);
|
||||
Loc:=SetMemInfoUInt64(Loc,Info.nLink);
|
||||
Loc:=SetMemInfoUInt64(Loc,Info.size);
|
||||
Loc:=SetMemInfoUInt64(Loc,Info.atim);
|
||||
Loc:=SetMemInfoUInt64(Loc,Info.mtim);
|
||||
Loc:=SetMemInfoUInt64(Loc,Info.ctim);
|
||||
Loc:=SetMemInfoUInt64(Loc,Info.atim*1000*1000);
|
||||
Loc:=SetMemInfoUInt64(Loc,Info.mtim*1000*1000);
|
||||
Loc:=SetMemInfoUInt64(Loc,Info.ctim*1000*1000);
|
||||
end;
|
||||
|
||||
function TPas2JSWASIEnvironment.path_filestat_get(fd, flags: NativeInt;
|
||||
@ -2064,7 +2099,7 @@ Var
|
||||
begin
|
||||
view:=getModuleMemoryDataView();
|
||||
view.setint16(aLoc,aValue, IsLittleEndian);
|
||||
Result:=aValue+SizeInt16;
|
||||
Result:=aLoc+SizeInt16;
|
||||
end;
|
||||
|
||||
function TPas2JSWASIEnvironment.SetMemInfoInt32(aLoc: TWasmMemoryLocation;
|
||||
@ -2076,7 +2111,7 @@ Var
|
||||
begin
|
||||
view:=getModuleMemoryDataView();
|
||||
view.setInt32(aLoc,aValue,IsLittleEndian);
|
||||
Result:=aValue+SizeInt32;
|
||||
Result:=aLoc+SizeInt32;
|
||||
end;
|
||||
|
||||
function TPas2JSWASIEnvironment.SetMemInfoInt64(aLoc: TWasmMemoryLocation;
|
||||
|
@ -34,7 +34,7 @@ Type
|
||||
function fd_prestat_get(fd: NativeInt; bufPtr: TWasmMemoryLocation) : NativeInt;
|
||||
function fd_pwrite(fd, iovs, iovsLen, offset, nwritten : NativeInt) : NativeInt;
|
||||
function fd_read(fd: NativeInt; iovs : TWasmMemoryLocation; iovsLen: NativeInt; nread : TWasmMemoryLocation) : NativeInt;
|
||||
function fd_readdir(fd, bufPtr, bufLen, cookie, bufusedPtr : NativeInt) : NativeInt;
|
||||
function fd_readdir(fd : NativeInt; bufPtr: TWasmMemoryLocation; bufLen, cookie: NativeInt; bufusedPtr : TWasmMemoryLocation) : NativeInt;
|
||||
function fd_renumber(afrom,ato : NativeInt) : NativeInt;
|
||||
function fd_seek(fd, offset, whence : NativeInt; newOffsetPtr : TWasmMemoryLocation) : NativeInt;
|
||||
function fd_sync(fd : NativeInt) : NativeInt;
|
||||
@ -646,6 +646,13 @@ type
|
||||
end;
|
||||
TWasiPreStat = __wasi_prestat_t;
|
||||
|
||||
TDirentType = (dtUnknown,dtFile,dtDirectory,dtSymlink,dtSocket,dtBlockDevice,dtCharacterDevice,dtFIFO);
|
||||
TWasiFSDirent = record
|
||||
ino: NativeInt;
|
||||
name : String;
|
||||
EntryType: TDirentType;
|
||||
next : NativeInt;
|
||||
end;
|
||||
|
||||
{ EWasiFSError }
|
||||
|
||||
@ -673,9 +680,21 @@ type
|
||||
function DataSync(FD : Integer) : NativeInt;
|
||||
function Seek(FD : integer; Offset : Integer; Whence : TSeekWhence; out NewPos : Integer) : NativeInt;
|
||||
Function Read(FD : Integer; Data : TJSUint8Array; AtPos : Integer; Out BytesRead : Integer) : NativeInt;
|
||||
function ReadDir(FD: Integer; Cookie: NativeInt; out DirEnt: TWasiFSDirent): NativeInt;
|
||||
Function GetPrestat(FD: Integer) : String;
|
||||
end;
|
||||
|
||||
Const
|
||||
DirentMap : Array [TDirentType] of Integer =
|
||||
(__WASI_FILETYPE_UNKNOWN,
|
||||
__WASI_FILETYPE_REGULAR_FILE,
|
||||
__WASI_FILETYPE_DIRECTORY,
|
||||
__WASI_FILETYPE_SYMBOLIC_LINK,
|
||||
__WASI_FILETYPE_SOCKET_STREAM,
|
||||
__WASI_FILETYPE_BLOCK_DEVICE,
|
||||
__WASI_FILETYPE_CHARACTER_DEVICE,
|
||||
__WASI_FILETYPE_UNKNOWN);
|
||||
|
||||
implementation
|
||||
|
||||
end.
|
||||
|
@ -5,21 +5,26 @@ unit wasizenfs;
|
||||
interface
|
||||
|
||||
uses
|
||||
JS, libzenfs, Wasitypes;
|
||||
SysUtils, JS, libzenfs, Wasitypes;
|
||||
|
||||
Type
|
||||
EWASIZenFS = class(Exception);
|
||||
|
||||
{ TWASIZenFS }
|
||||
|
||||
TWASIZenFS = class (TObject,IWasiFS)
|
||||
private
|
||||
FRoot : TZenFSDir;
|
||||
FDirMap : TJSMap;
|
||||
protected
|
||||
function AllocateDirFD(aDir: TZenFSDirentArray): Integer;
|
||||
function IsDirEnt(FD : Integer) : TZenFSDirentArray;
|
||||
Procedure RemoveDirent(FD : integer);
|
||||
function PrependFD(FD: Integer; aPath: String): string;
|
||||
class function ExceptToError(E : TJSObject) : Integer;
|
||||
class function ZenFSDateToWasiTimeStamp(aDate: TJSDate): Nativeint;
|
||||
class function ZenStatToWasiStat(ZSTat: TZenFSStats): TWasiFileStat;
|
||||
Public
|
||||
constructor create;
|
||||
Function MkDirAt(FD : Integer; const aPath : String) : NativeInt;
|
||||
Function RmDirAt(FD : Integer; const aPath : String) : NativeInt;
|
||||
function StatAt(FD : Integer; const aPath : String; var stat: TWasiFileStat) : NativeInt;
|
||||
@ -38,6 +43,7 @@ Type
|
||||
function DataSync(FD : Integer) : NativeInt;
|
||||
function Seek(FD : integer; Offset : Integer; Whence : TSeekWhence; out NewPos : Integer) : NativeInt;
|
||||
Function Read(FD : Integer; Data : TJSUint8Array; AtPos : Integer; Out BytesRead : Integer) : NativeInt;
|
||||
function ReadDir(FD: Integer; Cookie: NativeInt; out DirEnt: TWasiFSDirent): NativeInt;
|
||||
Function GetPrestat(FD: Integer) : String;
|
||||
end;
|
||||
|
||||
@ -48,6 +54,33 @@ const
|
||||
|
||||
{ TWASIZenFS }
|
||||
|
||||
function TWASIZenFS.AllocateDirFD(aDir : TZenFSDirentArray): Integer;
|
||||
var
|
||||
I : integer;
|
||||
|
||||
begin
|
||||
I:=4;
|
||||
While (I<100) and (FDirMap.has(i)) do
|
||||
Inc(I);
|
||||
if I=100 then
|
||||
Raise EWASIZenFS.Create('Too many directories');
|
||||
FDirMap.&set(I,aDir);
|
||||
Result:=I;
|
||||
end;
|
||||
|
||||
function TWASIZenFS.IsDirEnt(FD: Integer): TZenFSDirentArray;
|
||||
begin
|
||||
if FDirMap.has(FD) then
|
||||
Result:=TZenFSDirentArray(FDirMap.get(FD))
|
||||
else
|
||||
Result:=Nil;
|
||||
end;
|
||||
|
||||
procedure TWASIZenFS.RemoveDirent(FD: integer);
|
||||
begin
|
||||
FDirMap.delete(FD);
|
||||
end;
|
||||
|
||||
function TWASIZenFS.PrependFD(FD: Integer; aPath: String) : string;
|
||||
|
||||
begin
|
||||
@ -116,6 +149,11 @@ begin
|
||||
Result.ctim:=ZenFSDateToWasiTimeStamp(ZStat.cTime);
|
||||
end;
|
||||
|
||||
constructor TWASIZenFS.create;
|
||||
begin
|
||||
FDirMap:=TJSMap.new;
|
||||
end;
|
||||
|
||||
function TWASIZenFS.StatAt(FD: Integer; const aPath: String;
|
||||
var stat: TWasiFileStat): NativeInt;
|
||||
|
||||
@ -231,9 +269,16 @@ end;
|
||||
|
||||
function TWASIZenFS.Close(FD : Integer): NativeInt;
|
||||
|
||||
|
||||
begin
|
||||
try
|
||||
ZenFS.closeSync(fd);
|
||||
if IsDirEnt(FD)<>Nil then
|
||||
begin
|
||||
RemoveDirent(FD);
|
||||
Result:=WASI_ESUCCESS;
|
||||
end
|
||||
else
|
||||
ZenFS.closeSync(fd);
|
||||
Result:=resOK;
|
||||
except
|
||||
on E : TJSObject do
|
||||
@ -344,6 +389,38 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
function TWASIZenFS.ReadDir(FD: Integer; Cookie : NativeInt; out DirEnt: TWasiFSDirent): NativeInt;
|
||||
|
||||
var
|
||||
DirEnts : TZenFSDirentArray;
|
||||
ZDirEntry : TZenFSDirent;
|
||||
|
||||
begin
|
||||
DirEnts:=IsDirEnt(FD);
|
||||
if Not Assigned(DirEnts) then
|
||||
Exit(WASI_EBADF);
|
||||
if (Cookie<0) or (Cookie>=Length(Dirents)) then
|
||||
Exit(WASI_ENOENT);
|
||||
ZDirEntry:=Dirents[Cookie];
|
||||
DirEnt.name:=ZDirEntry.path;
|
||||
if ZDirEntry.isFile() then
|
||||
Dirent.EntryType:=dtFile
|
||||
else if ZDirEntry.isDirectory() then
|
||||
Dirent.EntryType:=dtDirectory
|
||||
else if ZDirEntry.isSymbolicLink() then
|
||||
Dirent.EntryType:=dtSymlink
|
||||
else if ZDirEntry.isFIFO() then
|
||||
Dirent.EntryType:=dtFIFO
|
||||
else if ZDirEntry.isBlockDevice() then
|
||||
Dirent.EntryType:=dtBlockDevice
|
||||
else if ZDirEntry.isCharacterDevice() then
|
||||
Dirent.EntryType:=dtCharacterDevice
|
||||
else if ZDirEntry.isSocket() then
|
||||
Dirent.EntryType:=dtSocket;
|
||||
Dirent.Next:=Cookie+1;
|
||||
Result:=ResOK;
|
||||
end;
|
||||
|
||||
function TWASIZenFS.GetPrestat(FD: Integer): String;
|
||||
begin
|
||||
if (FD=3) then
|
||||
@ -376,6 +453,8 @@ var
|
||||
lFlags : String;
|
||||
Rights : NativeInt;
|
||||
Reading,Writing : Boolean;
|
||||
Dir : TZenFSDirentArray;
|
||||
Opts : TZenFSReadDirOptions;
|
||||
|
||||
Function HasFlag(aFlag : Integer) : Boolean;
|
||||
begin
|
||||
@ -397,26 +476,37 @@ var
|
||||
begin
|
||||
if (fdFlags<>0) and (fsFlags<>0) then ;
|
||||
lPath:=PrependFD(FD,aPath);
|
||||
Rights:=AsIntNumber(fsRightsBase);
|
||||
Writing:=HasFlag(__WASI_OFLAGS_CREAT) or HasRight(__WASI_RIGHTS_FD_WRITE);
|
||||
Reading:=HasRight(__WASI_RIGHTS_FD_READ);
|
||||
if Writing then
|
||||
if Not HasFlag(__WASI_OFLAGS_DIRECTORY) then
|
||||
begin
|
||||
if HasFlag(__WASI_OFLAGS_TRUNC) then
|
||||
lFLags:='w'
|
||||
Rights:=AsIntNumber(fsRightsBase);
|
||||
Writing:=HasFlag(__WASI_OFLAGS_CREAT) or HasRight(__WASI_RIGHTS_FD_WRITE);
|
||||
Reading:=HasRight(__WASI_RIGHTS_FD_READ);
|
||||
if Writing then
|
||||
begin
|
||||
if HasFlag(__WASI_OFLAGS_TRUNC) then
|
||||
lFLags:='w'
|
||||
else
|
||||
lFLags:='a';
|
||||
if HasFlag(__WASI_OFLAGS_EXCL) then
|
||||
lFLags:=lFLags+'x';
|
||||
if Reading then
|
||||
lFLags:=lFLags+'+';
|
||||
end
|
||||
else
|
||||
lFLags:='a';
|
||||
if HasFlag(__WASI_OFLAGS_EXCL) then
|
||||
lFLags:=lFLags+'x';
|
||||
if Reading then
|
||||
lFLags:=lFLags+'+';
|
||||
end
|
||||
else
|
||||
begin
|
||||
lFlags:='r';
|
||||
begin
|
||||
lFlags:='r';
|
||||
end;
|
||||
end;
|
||||
try
|
||||
OpenFD:=ZenFS.openSync(lPath,lFlags);
|
||||
if HasFlag(__WASI_OFLAGS_DIRECTORY) then
|
||||
begin
|
||||
Opts:=TZenFSReadDirOptions.New;
|
||||
Opts.withFileTypes:=True;
|
||||
Dir:=ZenFS.readdirSyncDirent(lpath,Opts);
|
||||
OpenFD:=AllocateDirFD(Dir);
|
||||
end
|
||||
else
|
||||
OpenFD:=ZenFS.openSync(lPath,lFlags);
|
||||
Result:=resOK;
|
||||
except
|
||||
on E : TJSObject do
|
||||
|
@ -162,6 +162,7 @@ Type
|
||||
function isSocket() : Boolean;
|
||||
property path : string read FPath;
|
||||
end;
|
||||
TZenFSDirentArray = Array of TZenFSDirent;
|
||||
|
||||
TJSDirentCallback = reference to procedure(aDirent : TZenFSDirent);
|
||||
TJSDirCloseCallback = reference to procedure;
|
||||
@ -741,6 +742,8 @@ Type
|
||||
|
||||
Function readdirSync(path : String; options : TZenFSReadDirOptions): TStringDynArray; overload;
|
||||
Function readdirSync(path : String): TStringDynArray; overload;
|
||||
Function readdirSyncDirent(path : String; options : TZenFSReadDirOptions): TZenFSDirentArray; external name 'readdirSync';
|
||||
|
||||
|
||||
Procedure readlink(path : String; options : String; callback : TStringCallBack); overload;
|
||||
Procedure readlink(path : String; callback : TStringCallBack); overload;
|
||||
|
Loading…
Reference in New Issue
Block a user