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.