Download Now
Forums

    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.