Record Helpers
Did you know you can now extend intrinsic types, such as string, integer, and array with your own properties and methods? You can add your own methods to the
string
type like BeginsWith
, EndsWith
, Between
, and Split
. Here's how it works:type StringHelper = record helper for string private function GetLength: Integer; procedure SetLength(Value: Integer); public function BeginsWith(const SubStr: string; IgnoreCase: Boolean = False): Boolean; function EndsWith(const SubStr: string; IgnoreCase: Boolean = False): Boolean; function Between(const MarkerA, MarkerB: string): string; function Split(Separator: string): StringArray; property Length: Integer read GetLength write SetLength; end;
FTP client example
The following is a listing of an FTP client implementation which makes heavy use of record helpers on intrinsic type. With comments removed the implementation is a little more than 500 line s of code. What's interesting in my opinion is how much record helps can reduce code and improve readability.
Example: Using the PASV command:
'227 Entering Passive Mode (157,28,148,37,129,44)'
result from PASV at line 430 becomes
Values := S.Between('(', ')').SplitInt(',');
Values now is an array of 6 integers
Example: Using the LIST command:
'lrwxrwxrwx 1 0 0 3 Jul 02 2013 public -> pub'
result from LIST at line 637 becomes
Columns := S.Words(9);
Columns now is an array of 9 trimmed strings
Helper types can really make life easier!
Source code listing
{ <include docs/codebot.networking.ftp.txt> } unit Codebot.Networking.Ftp; {$i codebot.inc} interface uses Classes, SysUtils, Codebot.System, Codebot.Networking; { TFileSystemAttributes is a set of flags attached to a file system object See also <link Codebot.Networking.Ftp.TFileSystemAttributes, TFileSystemAttributes set> } type TFileSystemAttributes = set of ( { Item is a directory } fsaDirectory, { Item is a symbolic link } fsaLink, { Read permissions are set for the current user } fsaRead, { Write permissions are set for the current user } fsaWrite, { Execute permissions are set for the current user } fsaExecute); { fsaAny is a shortcut to all file system flagsSee also See also <link Codebot.Networking.Ftp.TFileSystemAttributes, TFileSystemAttributes set> } const fsaAny = [fsaDirectory, fsaLink, fsaRead, fsaWrite, fsaExecute]; { TRemoteFindData is used by <link Codebot.Networking.Ftp.TFtpClient.FindFirst, TFtpClient.FindFirst method> See also <link Codebot.Networking.Ftp.TFtpClient, TFtpClient class> <link Codebot.Networking.Ftp.TRemoteFindData, TRemoteFindData members> } type TRemoteFindData = record { Name of the remote item } Name: string; { Modified date of the remote item } Date: TDateTime; { Size in bytes of the remote item } Size: LargeWord; { Attributes describing the remote item } Attributes: TFileSystemAttributes; end; { TFtpClient provides access to an ftp client Remarks This class provides strictly synchronous operations See also <link Codebot.Networking.Ftp.TFtpClient, TFtpClient members> <link topic_networking, Accessing the Internet topic> } TFtpClient = class(TObject) private FCommand: TSocket; FHost: string; FPort: Word; FUserName: string; FPassword: string; FTransfering: Boolean; FFindMask: TFileSystemAttributes; FFindList: StringArray; FFindIndex: Integer; FOnCommand: TTextEvent; FOnResponse: TTextEvent; FOnProgress: TTransferEvent; type TResponse = record public Valid: Boolean; Raw: string; Code: Integer; Message: string; function IsFail(Low, High: Integer): Boolean; function IsPass(Low, High: Integer): Boolean; end; function FileModeBinary: Boolean; function Passive(out Socket: TSocket): Boolean; procedure Send(const S: string; out R: TResponse); procedure Recv(out R: TResponse); procedure SetConnected(Value: Boolean); function GetConnected: Boolean; protected { Invoke the OnProgress event } procedure DoProgress(const Size, Sent: LargeWord); public { Create a new file transfer object } constructor Create; destructor Destroy; override; { Attempt to open a file transfer connection using the host, port, username, and password } function Connect: Boolean; { Close any opened connection } procedure Disconnect; { Cancel any ongoing transfers } procedure Cancel; { Returns the current remote directory } function GetCurrentDir: string; { Returns true if a remote directory exists } function DirExists(const Dir: string): Boolean; { Change to a new current remote directory } function ChangeDir(const Dir: string): Boolean; { Create a new remote directory } function MakeDir(const Dir: string): Boolean; { Delete an existing remote directory } function RemoveDir(const Dir: string): Boolean; { Delete an existing remote file } function FileDelete(const FileName: string): Boolean; { Returns true if a remote file exists } function FileExists(const FileName: string): Boolean; { Rename a remote file, works with directories too } function FileRename(const OldName, NewName: string): Boolean; { Retrieve the size of a remote file } function FileSize(const FileName: string): LargeWord; { Retrieve the modified date of a remote file } function FileDate(const FileName: string): TDateTime; { Initiate an file upload to the remote server } function FilePut(const LocalFile, RemoteFile: string; Overwrite: Boolean = True): Boolean; { Request a file download from the remote server } function FileGet(const RemoteFile, LocalFile: string; Overwrite: Boolean = True): Boolean; { Retrieve a text mode listing files and folders } function FileList(const Path: string = ''): string; { Initiate a structured listing files and folders with an optional attribute mask } function FindFirst(const Path: string; out FindData: TRemoteFindData; Allow: TFileSystemAttributes = fsaAny): Boolean; { Continue with the next listing started by FindFirst } function FindNext(out FindData: TRemoteFindData): Boolean; { Returns true when connected to a remote server, otherwise acts like connect and disconnect } property Connected: Boolean read GetConnected write SetConnected; { The name of the host to resolve when connecting } property Host: string read FHost write FHost; { The port used for issuing ftp commands, defaults to 21 } property Port: Word read FPort write FPort; { The username used when connecting, defaults to anonymous } property UserName: string read FUserName write FUserName; { The password used when connecting, defaults to an email address } property Password: string read FPassword write FPassword; { An event invoked echoing ftp commands issued by the client } property OnCommand: TTextEvent read FOnCommand write FOnCommand; { An event invoked when responses are read from the remote server } property OnResponse: TTextEvent read FOnResponse write FOnResponse; { An event continuously invoked as file transfers occur } property OnProgress: TTransferEvent read FOnProgress write FOnProgress; end; implementation { TResponse } function TFtpClient.TResponse.IsFail(Low, High: Integer): Boolean; begin if not Valid then Result := True else Result := not Code.Between(Low, High); end; function TFtpClient.TResponse.IsPass(Low, High: Integer): Boolean; begin Result := Valid and Code.Between(Low, High); end; { TFtpClient } constructor TFtpClient.Create; begin inherited Create; FCommand := TSocket.Create; FCommand.Timeout := 1; FHost := 'localhost'; FUserName := 'anonymous'; FPassword := 'user@email.com'; FPort := 21; end; destructor TFtpClient.Destroy; begin Disconnect; FCommand.Free; inherited Destroy; end; procedure TFtpClient.Send(const S: string; out R: TResponse); var Args: TTextEventArgs; begin R.Valid := False; R.Code := 0; R.Message := ''; R.Raw := ''; if not FCommand.Connected then Exit; Args.Text := S; if Assigned(FOnCommand) then FOnCommand(Self, Args); FCommand.Write(Args.Text + #13#10); Recv(R); end; procedure TFtpClient.Recv(out R: TResponse); var Args: TTextEventArgs; S: string; begin R.Valid := False; R.Code := 0; R.Message := ''; R.Raw := ''; if not FCommand.Connected then Exit; Args.Text := ''; while FCommand.Read(S, 3000) > 0 do Args.Text := Args.Text + S; if Assigned(FOnResponse) then FOnResponse(Self, Args); R.Raw := Args.Text; R.Message := R.Raw.Trim.AdjustLineBreaks(tlbsCRLF); R.Message := R.Message.Split(#13#10).Pop; R.Code := StrToIntDef(R.Message.FirstOf(' '), 0); R.Valid := R.Code > 0; if R.Valid then R.Message := R.Message.SecondOf(' ').Trim else R.Message := ''; end; function TFtpClient.Connect: Boolean; var R: TResponse; begin Disconnect; if FHost.IsWhitespace or FUserName.IsWhitespace or FPassword.IsWhitespace or (FPort = 0) then Exit(False); Result := True; if FCommand.Connect(FHost, Port) then Recv(R) else Exit(False); if R.IsFail(200, 299) then begin Disconnect; Exit; end; Send('USER ' + FUserName, R); if R.IsFail(200, 399) then begin Disconnect; Exit; end; Send('PASS ' + FPassword, R); if R.IsFail(200, 299) then begin Disconnect; Exit; end; end; procedure TFtpClient.Disconnect; var R: TResponse; begin Cancel; Send('QUIT', R); FCommand.Close; end; procedure TFtpClient.Cancel; var R: TResponse; begin if FTransfering then begin Send('ABOR', R); FTransfering := False; end; end; function TFtpClient.GetCurrentDir: string; var R: TResponse; begin Send('PWD', R); if R.IsPass(200, 299) then begin if R.Message.Contains('"') then Result := R.Message.Between('"', '"') else if R.Message.Contains('''') then Result := R.Message.Between('''', '''') else Result := R.Message.Trim.FirstOf(' '); end else Result := ''; end; function TFtpClient.DirExists(const Dir: string): Boolean; var R: TResponse; S: string; begin Result := False; S := GetCurrentDir; if S = '' then Exit; Send('CWD ' + Dir.Quote, R); if R.IsPass(200, 299) then begin Result := True; Send('CWD ' + S.Quote, R); end; end; function TFtpClient.ChangeDir(const Dir: string): Boolean; var R: TResponse; begin Send('CWD ' + Dir.Quote, R); Result := R.IsPass(200, 299); end; function TFtpClient.MakeDir(const Dir: string): Boolean; var R: TResponse; begin Send('MKD ' + Dir.Quote, R); Result := R.IsPass(200, 299); end; function TFtpClient.RemoveDir(const Dir: string): Boolean; var R: TResponse; begin Send('RMD ' + Dir.Quote, R); Result := R.IsPass(200, 299); end; function TFtpClient.FileDelete(const FileName: string): Boolean; var R: TResponse; begin Send('DELE ' + FileName.Quote, R); Result := R.IsPass(200, 299); end; function TFtpClient.FileExists(const FileName: string): Boolean; var R: TResponse; begin Send('SIZE ' + FileName.Quote, R); Result := R.IsPass(200, 299); end; function TFtpClient.FileRename(const OldName, NewName: string): Boolean; var R: TResponse; begin Send('RNFR ' + OldName.Quote, R); if R.IsPass(200, 299) then begin Send('RNTO ' + NewName.Quote, R); Result := R.IsPass(200, 299); end else Result := False; end; function TFtpClient.FileSize(const FileName: string): LargeWord; var R: TResponse; begin Send('SIZE ' + FileName.Quote, R); if R.IsPass(200, 299) then Result := StrToQWordDef(R.Message, 0) else Result := 0; end; function TFtpClient.FileDate(const FileName: string): TDateTime; var R: TResponse; S: string; Year, Month, Day, Hour, Minute, Second: Word; begin Result := 0; Send('MDTM ' + FileName.Quote, R); if R.IsPass(200, 299) then begin S := R.Message.Trim; if S.Length <> 'YYYYMMDDhhmmss'.Length then Exit; Year := StrToIntDef(S.Copy(1, 4), 1970); Month := StrToIntDef(S.Copy(5, 2), 1); Day := StrToIntDef(S.Copy(7, 2), 1); Hour := StrToIntDef(S.Copy(9, 2), 0); Minute := StrToIntDef(S.Copy(11, 2), 0); Second := StrToIntDef(S.Copy(13, 2), 0); Result := EncodeDate(Year, Month, Day) + EncodeTime(Hour, Minute, Second, 0); end; end; function TFtpClient.Passive(out Socket: TSocket): Boolean; var R: TResponse; V: IntArray; begin Socket := nil; Result := False; Send('PASV', R); if R.IsPass(200, 299) then begin R.Message := R.Message.Between('(', ')'); V := R.Message.SplitInt(','); if V.Length <> 6 then Exit; Socket := TSocket.Create; try Socket.Timeout := 1; Result := Socket.Connect('%d.%d.%d.%d'.Format([V[0], V[1], V[2], V[3]]), V[4] * 256 + V[5]); finally if not Result then Socket.Free; end; end end; function TFtpClient.FileModeBinary: Boolean; var R: TResponse; begin Send('TYPE I', R); Result := R.IsPass(200, 299); end; procedure TFtpClient.DoProgress(const Size, Sent: LargeWord); var Args: TTransferArgs; begin if Assigned(FOnProgress) then begin Args.Size := Size; Args.Sent := Sent; FOnProgress(Self, Args); end; end; function TFtpClient.FilePut(const LocalFile, RemoteFile: string; Overwrite: Boolean = True): Boolean; const BufferSize = 1024 * 1024; var Socket: TSocket; Stream: TStream; Buffer: Pointer; SourceSize, DestSize: LargeWord; Count: LongWord; R: TResponse; begin Result := False; if not Codebot.System.FileExists(LocalFile) then Exit; if (not Overwrite) and FileExists(RemoteFile) then Exit; if not FileModeBinary then Exit; SourceSize := Codebot.System.FileSize(LocalFile); DestSize := 0; if Passive(Socket) then try Send('STOR ' + RemoteFile.Quote, R); if R.IsFail(100, 299) then Exit; Stream := TFileStream.Create(LocalFile, fmOpenRead); GetMem(Buffer, BufferSize); FTransfering := True; try repeat Count := Stream.Read(Buffer^, BufferSize); if Count > 0 then if Socket.WriteAll(Buffer^, Count) then begin DestSize := DestSize + Count; DoProgress(SourceSize, DestSize); end; until (not FTransfering) or (Count < BufferSize); Result := DestSize = SourceSize; finally FTransfering := False; FreeMem(Buffer); Stream.Free; end; finally Socket.Free; end; end; function TFtpClient.FileGet(const RemoteFile, LocalFile: string; Overwrite: Boolean = True): Boolean; const BufferSize = 1024 * 1024; var Socket: TSocket; Stream: TStream; Buffer: Pointer; SourceSize, DestSize: LargeWord; Count: LongWord; R: TResponse; begin Result := False; if (not Overwrite) and Codebot.System.FileExists(LocalFile) then Exit; if not FileModeBinary then Exit; SourceSize := FileSize(RemoteFile); DestSize := 0; if Passive(Socket) then try Send('RETR ' + RemoteFile.Quote, R); if R.IsFail(100, 299) then Exit; Stream := TFileStream.Create(LocalFile, fmCreate); GetMem(Buffer, BufferSize); FTransfering := True; try repeat Count := Socket.Read(Buffer^, BufferSize); if Count > 0 then if Stream.Write(Buffer^, Count) = Count then begin DestSize := DestSize + Count; DoProgress(SourceSize, DestSize); end; until (not FTransfering) or (Count < 1); Result := DestSize = SourceSize; finally FTransfering := False; FreeMem(Buffer); Stream.Free; end; finally Socket.Free; end; end; function TFtpClient.FileList(const Path: string = ''): string; var Socket: TSocket; R: TResponse; S: string; begin Result := ''; if Passive(Socket) then try if not Path.IsWhitespace then Send('LIST', R) else Send('LIST ' + Path, R); if R.IsPass(200, 299) then while Socket.Read(S) > 0 do Result := Result + S; finally Socket.Free; end; end; function TFtpClient.FindFirst(const Path: string; out FindData: TRemoteFindData; Allow: TFileSystemAttributes = fsaAny): Boolean; var S: string; begin S := FileList.Trim; if S.IsEmpty then begin FindData.Name := ''; FindData.Date := 0; FindData.Size := 0; FindData.Attributes := []; Result := False; end else begin S := S.AdjustLineBreaks(tlbsCRLF); FFindMask := Allow; FFindList := S.Split(#13#10); FFindIndex := -1; Result := FindNext(FindData); end; end; function TFtpClient.FindNext(out FindData: TRemoteFindData): Boolean; function CurrentYear: Word; var Year, Month, Day: Word; begin DecodeDate(Now, Year, Month, Day); Result := Year; end; const AttributeColumn = 0; SizeColumn = 4; MonthColumn = 5; DayColumn = 6; YearColumn = 7; FileColumn = 8; var Columns: StringArray; Coded: Boolean; S: string; Y, M, D: Word; T: Double; I: Integer; begin FindData.Name := ''; FindData.Date := 0; FindData.Size := 0; FindData.Attributes := []; Inc(FFindIndex); if FFindIndex < FFindList.Length then begin Columns := FFindList[FFindIndex].Words(FileColumn); S := Columns[AttributeColumn]; if S[1] = 'd' then Include(FindData.Attributes, fsaDirectory); if S[1] = 'l' then Include(FindData.Attributes, fsaLink); if S[8] = 'r' then Include(FindData.Attributes, fsaRead); if S[9] = 'w' then Include(FindData.Attributes, fsaWrite); if S[10] = 'x' then Include(FindData.Attributes, fsaExecute); if FindData.Attributes * FFindMask = [] then begin Result := FindNext(FindData); Exit; end; FindData.Name := Columns[FileColumn]; FindData.Size := StrToQWordDef(Columns[SizeColumn], 0); M := 1; for I := Low(FormatSettings.ShortMonthNames) to High(FormatSettings.ShortMonthNames) do if Columns[MonthColumn].Equals(FormatSettings.ShortMonthNames[I], True) then begin M := I; Break; end; D := StrToIntDef(Columns[DayColumn], 1); S := Columns[YearColumn]; Coded := S.Contains(':'); if Coded then begin Y := CurrentYear; T := StrToTime(S + ':00'); end else begin Y := StrToIntDef(S, CurrentYear); T := 0; end; FindData.Date := EncodeDate(Y, M, D) + T; if Coded and (FindData.Date > Now + 1) then FindData.Date := EncodeDate(Y - 1, M, D) + T; Result := True; end else Result := False; end; procedure TFtpClient.SetConnected(Value: Boolean); begin if Value <> FCommand.Connected then begin if Value then Connect else Disconnect; end; end; function TFtpClient.GetConnected: Boolean; begin Result := FCommand.Connected; end; end.