Глава 6. CsShopper: FTP-клиент

Джон Пенман

Отправляйтесь в Internet за бесплатным барахлом! В этом вам поможет компонент, выполняющий функции FTP-клиента, и полноцен ное приложение для пересылки файлов, построенное на его основе.

Популярность Internet в немалой степени обусловлена возможностью обмена информацией между компьютерами. Такой обмен становится возможным благодаря протоколу пересылки файлов FTP (File Transfer Protocol) — одному из самых старых протоколов, используемых в Internet. Формальная спецификация используемого в настоящее время протокола FTP содержится в документе RFC959.

Протокол FTP, как и другие Internet-протоколы, берет свое начало в классической модели клиент/сервер. FTP-сервер иногда представляется мне в виде старомодного продавца, который снимает товар с полки и передает его покупателю (FTP-клиенту). В этой главе мы реализуем компонент Delphi с весьма подходящим именем CsShopper, выполняющий функции FTP-клиента.

Компонент CsShopper построен на основе CsSocket — простейшего компонента-оболочки для функций Winsock API, созданного в главе 5. CsSocket обеспечивает базовые возможности, необходимые для работы протокола FTP в сети TCP/IP. Таким образом, о мелочах есть кому позаботиться, и мы можем сразу же прейти к более пристальному рассмотрению процесса FTP глазами клиента.

Вас обслуживают?

По умолчанию FTP-сервер всегда ожидает, что клиент инициирует соедине ние через TCP-порт с номером 21. Это соединение (оно называется управляющим соединением, control connection) остается открытым до тех пор, пока либо клиент, либо сервер не закроет его со своей стороны. Через установлен ное соединение клиент и сервер обмениваются командами FTP и кодами ответов соответственно. В командах Internet-протоколов обычно используется обычный англоязычный текст (чаще всего в верхнем регистре). Это остается справедливым даже при взаимодействиях между программами. Причина заключается в том, что Internet первоначально работал только с 7-разрядной ASCII-кодировкой, которая была (и остается) «наименьшим общим знамена телем» для общения двух систем — компьютерных или любых других.

Это обстоятельство не лучшим образом сказывается на скорости работы, но зато человеку становится значительно легче уследить за взаимодействием двух Internet-программ. На каждую команду, полученную от клиента, сервер обычно посылает код ответа. Код состоит из трех цифр, за которыми следует дефис или пробел, а затем — некоторый текст. Типичные сообщения могут выглядеть следующим образом:

200 PORT command successful.
230-Welcome to your I-SITE Internet server!

Дефис или пробел, следующий за числовым кодом, содержит важную для клиента информацию. Дефис сообщает клиенту о том, что данное сообщение является комментарием и его можно спокойно игнорировать. Пробел указывает клиенту на необходимость перехода к следующей фазе текущей операции. Текст, который идет дальше, обычно содержит информацию о статусе или инструкцию для пользователя.

Диаграмма, изображенная на рис. 6.1, описывает взаимодействие клиента с сервером во время регистрации. FTP-сеанс начинается с посылки клиентом команды USER, за которой следует имя пользователя, и получения со стороны сервера кода ответа, состоящего из трех цифр. Если имя пользовате ля признается допустимым, сервер отвечает кодом 331 или 230. При недопустимом имени пользователя генерируется код 4xx или 5xx, где xx описывает код конкретной ошибки.

Ответ 230 означает, что имя пользователя признано допустимым и для доступа к системе не требуется никакой дополнительной информации. Сервер обычно выдает этот код в ответ при знаменитой «анонимной» регистрации пользователей. Ответ 331 означает, что имя пользователя также признано допустимым, но для доступа к системе необходим пароль. В этом случае клиент посылает команду PASS, за которой следует пароль.

Неверный пароль вызывает ответ 4xx или 5xx, свидетельствующий об ошибке. Если пароль принят, сервер может послать код 230, чтобы сообщить о завершении регистрации. Если для регистрации необходимы сведения об используемых ресурсах (account), сервер снова отвечает кодом 331, чтобы клиент послал команду ACCT и требуемые сведения.

Рис. 6.1. Регистрация FTP-клиента на FTP-сервере

После того как соединение будет успешно установлено, клиент может продолжить посылку команд. Однако при возникновении проблемы (например, посылке команды с неверным синтаксисом) или слишком большом количестве пользователей, работающих в системе, сервер посылает код 4xx или 5xx и закрывает соединение.

Компонент CsShopper

CsShopper происходит от VCL-компонента CsSocket из главы 5. В нем класс TCsSocket используется для выполнения повседневных задач — загрузки Winsock DLL, заполнения структур данных для установки соединения с хостом, пересылки данных, разрыва соединения с сервером и последующего закрытия Winsock.

Свойство Service базового VCL-компонента CsSocket имеет значение NoService. Компонент CsShopper всегда выполняет функции FTP-клиента, поэтому в конструкторе TCsShopper.Create свойство Service получает значение FTP. В остальном протокол FTP использует стандартные настройки CsSocket — все-таки отличная штука эти компоненты! Как показано на рис. 6.2, помимо Service CsShopper содержит 10 других свойств: Access, AddrType, Asynchronous, Debug, HomeServer, LogOn, Password, Protocol, SockType и UserName.

Рис. 6.2. Свойства CsShopper в инспекторе
объектов Delphi 3

Свойство Asynchronous определяет режим работы CsShopper — блокирующий или асинхронный . Хотя данное свойство не относится к протоколу FTP, выбор режима может повлиять на скорость пересылки данных, надежность приложения и его гибкость. Например, когда CsShopper работает в асинхронном режиме (то есть свойство Asynchronous равно TRUE), пользователь может прервать чересчур затянувшуюся пересылку файла. В блокирующем режиме такая возможность отсутствует (впрочем, если ChShopper написан как многопоточное приложение, то пересылку файла можно прервать и в блокирующем режиме, но это совсем другая история).

Асинхронный режим устроен несколько сложнее, поэтому сначала мы посмотрим, как CsShopper работает в блокирующем режиме. Асинхронный режим будет описан позднее в этой главе.

Самые полезные FTP-команды (в том числе USER, PASSWORD, RETR и PUT) реализованы в CsShopper в виде свойств. Эти свойства находятся в public-секции TCsShopper и потому доступны для пользователей компонента. В блокирующем режиме соответствующие методы используют процедуру FTPCommand, которая является «сердцем» компонента CsShopper. FTPCommand представляет собой простейший анализатор, реализованный в виде большого оператора case. Недостаток изящества подобной конструкции возмещается ее простотой. В асинхронном режиме CsShopper использует другой подход.

Полный исходный текст компонента, находящийся в файле CSSHOPPER.PAS, занимает около 3000 строк, и я не стал включать его в эту главу. Будут приведены лишь отдельные фрагменты, поясняющие некоторые аспекты его работы. Для более подробного знакомства вы можете распечатать полный файл
с CD-ROM.

Организация вывода

Хотя CsShopper и относится к невизуальным компонентам, время от времени ему приходится взаимодействовать с приложением пользователя и отображать сообщения, которыми сервер обменивается с клиентом. Такую возможность предоставляет published-свойство OnInfo класса TCsShopper (унаследован ное от класса TCsSocket) и private-процедура InfoEvent. Процедура InfoEvent выглядит следующим образом:

procedure TCsSocket.InfoEvent(Msg : String);
begin
if Assigned(FOnInfoEvent) then
FOnInfoEvent(Self, Msg);
end;

Когда через управляющее соединение отправляется или принимается сообщение, локальная переменная TempStr в процедуре FTPCommand задает значение свойства Info, после чего FTPCommand вызывает процедуру InfoEvent. Внутри InfoEvent проверка Assignеd возвращает значение True, а процедура CsShopper1Info из приложения отображает Info.

Чтобы такое взаимодействие между CsShopper и клиентским приложением стало возможным, я создал процедуру CsShopper1Info с помощью вкладки Events инспектора объектов. Содержимое окна memLog, в котором отображаются все эти сообщения, обновляется с каждым событием FOnInfoEvent. CsShopper1Info содержит следующий фрагмент кода:

procedure TfrmMain.CsShopper1Info(Sender: TObject; Msg: String);
begin
memLog.Lines.Add(Msg);
end;

SHOPPER32 за работой

SHOPPER32 — базовое FTP-приложение, созданное с помощью компонента CsShopper, оно изображено на рис. 6.3. Создайте новый проект с именем SHOPPER32, вызовите главную форму frmMain и сохраните в модуле MAIN.PAS содержимое листинга 6.1.

Листинг 6.1. Модуль MAIN.PAS (* Модуль Main Написан для книги High Performance Delphi Programming - Джон К.Пенман 1997 За дополнительной информацией и помощью обращайтесь по адресу info@craiglockhart.com *)


unit main;



interface



uses

  Windows, Messages, SysUtils, Classes,

  Graphics, Controls, Forms,

  Dialogs, StdCtrls, Buttons, FileCtrl,

  ComCtrls, CsSocket,

  CsShopper, MkDirFrm, CsFtpMsg, ToolWin,

  Registry, ExtCtrls;

Рис. 6.3. Приложение SHOPPER32


type

  TfrmMain = class(TForm)

    CsShopper1: TCsShopper;

    pcShopper: TPageControl;

    tsConnect: TTabSheet;

    tsOptions: TTabSheet;

    tsAbout: TTabSheet;

    gbLocal: TGroupBox;

    gbRemote: TGroupBox;

    gbActions: TGroupBox;

    dcbLocal: TDriveComboBox;

    dlbLocal: TDirectoryListBox;

    flbLocal: TFileListBox;

    sbStatus: TStatusBar;

    pbDataTransfer: TProgressBar;

    lbRemoteFiles: TListBox;

    bbtnExit: TBitBtn;

    bbtnConnect: TBitBtn;

    bbtnAbort: TBitBtn;

    gbUserName: TGroupBox;

    gbPassword: TGroupBox;

    gbDefLocalDir: TGroupBox;

    gbDefTextEditor: TGroupBox;

    edDefUserName: TEdit;

    edDefPassword: TEdit;

    edDefLocalDir: TEdit;

    edDefTextEditor: TEdit;

    bbtnFtpCmds: TBitBtn;

    bbtnLocateTxtEditor: TBitBtn;

    bbtnLocateDefLocalDir: TBitBtn;

    gbMoreActions: TGroupBox;

    bbtnRefresh: TBitBtn;

    bbtnFTPHelp: TBitBtn;

    bbtnSite: TBitBtn;

    bbtnNewDir: TBitBtn;

    bbtnDelDir: TBitBtn;

    bbtnViewFile: TBitBtn;

    memLog: TMemo;

    rgFileType: TRadioGroup;

    bbtnRestart: TBitBtn;

    bbtnQuit: TBitBtn;

    tsProfiles: TTabSheet;

    gbSetProfile: TGroupBox;

    gbPrName: TGroupBox;

    gbPrHostName: TGroupBox;

    gbPrUserName: TGroupBox;

    gbPrPassWord: TGroupBox;

    gbPrRemDir: TGroupBox;

    gbPrLocDir: TGroupBox;

    edPrName: TEdit;

    edPrHostName: TEdit;

    edPrUserName: TEdit;

    edPrPassword: TEdit;

    edPrRemDir: TEdit;

    edPrLocDir: TEdit;

    gbPrList: TGroupBox;

    lbPrList: TListBox;

    bbtnPrNew: TBitBtn;

    bbtnPrSave: TBitBtn;

    bbtnPrDelete: TBitBtn;

    rgFTPMode: TRadioGroup;

    sbbtnRetr: TSpeedButton;

    sbbtnStor: TSpeedButton;

    Panel1: TPanel;

    Label1: TLabel;

    Label2: TLabel;

    Label3: TLabel;

    Label4: TLabel;

    Label5: TLabel;

    bbtnStat: TBitBtn;

    gbHints: TGroupBox;

    cbHints: TCheckBox;

    gbFTPOptions: TGroupBox;

    BitBtn2: TBitBtn;

    rgFileStructure: TRadioGroup;

    rgTransfer: TRadioGroup;

    bbtnAddNew: TBitBtn;

    procedure bbtnConnectClick(Sender: TObject);

    procedure FormCreate(Sender: TObject);

    procedure bbtnFtpCmdsClick(Sender: TObject);

    procedure CsShopper1Info(Sender: TObject;

    Msg: String);

    procedure CsShopper1UpDateList(Sender:

    TObject; List: TStringList);

    procedure lbRemoteFilesDblClick(Sender:

    TObject);

    procedure CsShopper1List(Sender:

    TObject; List: TStringList);

    procedure bbtnSiteClick(Sender: TObject);

    procedure bbtnFTPHelpClick(Sender: TObject);

    procedure CsShopper1Busy(Sender:

    TObject; BusyFlag: Boolean);

    procedure CsShopper1Progress(Sender: TObject;

    Position: Integer);

    procedure rgFileTypeClick(Sender: TObject);

    procedure CsShopper1FileType(Sender: TObject;

    FileType: TFileTypes);

    procedure CsShopper1Error(Sender: TObject;

    Status: TConditions;

      Msg: String);

    procedure bbtnNewDirClick(Sender: TObject);

    procedure bbtnDelDirClick(Sender: TObject);

    procedure CsShopper1Connect(Sender: TObject;

    sSocket: Integer);

    procedure bbtnQuitClick(Sender: TObject);

    procedure rgFTPModeClick(Sender: TObject);

    procedure bbtnRefreshClick(Sender: TObject);

    procedure sbbtnRetrClick(Sender: TObject);

    procedure sbbtnStorClick(Sender: TObject);

    procedure CsShopper1DataDone(Sender: TObject;

    Done: Boolean);

    procedure bbtnStatClick(Sender: TObject);

    procedure bbtnRestartClick(Sender: TObject);

    procedure flbLocalDblClick(Sender: TObject);

    procedure lbRemoteFilesClick(Sender: TObject);

    procedure flbLocalClick(Sender: TObject);

    procedure lbPrListDblClick(Sender: TObject);

    procedure bbtnConnectMouseDown(Sender:

    TObject; Button: TMouseButton;

      Shift: TShiftState; X, Y: Integer);

    procedure bbtnViewFileClick(Sender: TObject);

    procedure bbtnAbortClick(Sender: TObject);

    procedure bbtnPrSaveClick(Sender: TObject);

    procedure bbtnExitClick(Sender: TObject);

    procedure lbPrListClick(Sender: TObject);

    procedure bbtnPrNewClick(Sender: TObject);

    procedure bbtnAddNewClick(Sender: TObject);

    procedure edPrNameExit(Sender: TObject);

    procedure edPrHostNameExit(Sender: TObject);

    procedure edPrUserNameExit(Sender: TObject);

    procedure edPrPasswordExit(Sender: TObject);

    procedure edPrRemDirExit(Sender: TObject);

    procedure edPrLocDirExit(Sender: TObject);

    procedure bbtnPrDeleteClick(Sender: TObject);

    procedure bbtnLocateDefLocalDirClick(Sender

    : TObject);

    procedure bbtnLocateTxtEditorClick(Sender:

    TObject);

    procedure BitBtn2Click(Sender: TObject);

  private

    { Private declarations }

  public

    { Public declarations }

   HelpCmd  : String;

   UsedProfile,

   UsedQFTP,

   NewProfile : Boolean;

   OldTransferMode,

   OldFileStruct : String;

   OldProfiles,

   HostNameList,

   UsernameList,

   PasswordList,

   RemoteDirList,

   LocalDirList,

   CurrentProfiles,

   ProfileNameList : TStringList;

   NoOfUsers,

   LastProfileUsed,

   NoProfiles : Integer;

   procedure LoadSettings;

   procedure SaveOptions;

   procedure SaveProfiles;

  end;



var

  frmMain: TfrmMain;



implementation



uses RMDirFrm, HelpFrm, QuickFTPfrm,

LocateDirFrm, LocateEdFrm;



{$R *.DFM}





const

     FtpClientKey = 'Software\High Performance

     Delphi\Shopper32';

procedure TfrmMain.LoadSettings;

var

 Reg      : TRegistry;

 Count    : Integer;

 ProfileName   : String;

begin

 Reg := TRegistry.Create;

// Считываем имя пользователя по умолчанию

 try

  Reg.OpenKey(FtpClientKey, TRUE);

  if Reg.ValueExists('UserName') then

   edDefUserName.Text := Reg.ReadString('UserName')

  else

   edDefUserName.Text := 'anonymous';

  finally

   Reg.CloseKey;

  end;

// Считываем пароль по умолчанию

  try

   Reg.OpenKey(FtpClientKey, TRUE);

  if Reg.ValueExists('Password') then

   edDefPassword.Text := Reg.ReadString('Password')

  else

   edDefPassword.Text := 'guest';

  finally

   Reg.CloseKey;

  end;

// Считываем локальный каталог по умолчанию

  try

   Reg.OpenKey(FtpClientKey, TRUE);

  if Reg.ValueExists('DefLocalDir') then

   edDefLocalDir.Text

   := Reg.ReadString('DefLocalDir')

  else

   edDefLocalDir.Text := 'C:\';

  finally

   Reg.CloseKey;

  end;

// Считываем редактор, используемый по умолчанию

  try

   Reg.OpenKey(FtpClientKey, TRUE);

  if Reg.ValueExists('Editor') then

   edDefTextEditor.Text

   := Reg.ReadString('Editor')

  else

   edDefTextEditor.Text := 'NOTEPAD ';

  finally

   Reg.CloseKey;

  end;

// Задаем свойства

 try

  Reg.OpenKey(FtpClientKey, TRUE);

  if Reg.ValueExists('Asynchronous') then

  begin

   with CsShopper1 do

   begin

    Asynchronous

    := Reg.ReadBool('Asynchronous');

    if Asynchronous then

     rgFTPMode.ItemIndex := 0

    else

     rgFTPMode.ItemIndex := 1;

   end;

  end

  else

  begin

   CsShopper1.Asynchronous := FALSE;

   rgFTPMode.ItemIndex := 0;

  end;

 finally

  Reg.CloseKey;

 end;

 try

  Reg.OpenKey(FtpClientKey, TRUE);

  if Reg.ValueExists('Hints') then

   cbHints.Checked := Reg.ReadBool('Hints')

  else

   cbHints.Checked := FALSE;

 finally

  Reg.CloseKey;

 end;

 try

  Reg.OpenKey(FtpClientKey, TRUE);

  if Reg.ValueExists('DTransferMode') then

  begin

   OldTransferMode

   := Reg.ReadString('DTransferMode');

   if UpperCase(OldTransferMode)

   = UpperCase(FtpTransferStr[STREAM]) then

   begin

    CsShopper1.Transfer := STREAM;

    rgTransfer.ItemIndex := 0;

   end;

   if UpperCase(OldTransferMode)

   = UpperCase(FtpTransferStr[BLOCK]) then

   begin

    CsShopper1.Transfer := BLOCK;

    rgTransfer.ItemIndex := 1;

   end;

   if UpperCase(OldTransferMode)

   = UpperCase(FtpTransferStr[COMPRESSED]) then

   begin

    CsShopper1.Transfer := COMPRESSED;

    rgTransfer.ItemIndex := 2;

   end;

  end else

  begin

   OldTransferMode

   := UpperCase(FtpTransferStr[STREAM]);

   CsShopper1.Transfer := STREAM;

   rgTransfer.ItemIndex := 0;

  end;

 finally

  Reg.CloseKey;

 end;

// Свойство файловой структуры

 try

  Reg.OpenKey(FtpClientKey, TRUE);

  if Reg.ValueExists('DFileStructure') then

  begin

   OldFileStruct := Reg.ReadString('DFileStructure');

   if UpperCase(OldFileStruct)

   = UpperCase(FtpFileStructStr[NOREC]) then

   begin

    CsShopper1.FileStruct := NOREC;

    rgFileStructure.ItemIndex := 0;

   end;

   if UpperCase(OldFileStruct)

   = UpperCase(FtpFileStructStr[REC]) then

   begin

    CsShopper1.FileStruct := REC;

    rgFileStructure.ItemIndex := 1;

   end;

   if UpperCase(OldFileStruct)

   = UpperCase(FtpFileStructStr[PAGE]) then

   begin

    CsShopper1.FileStruct := PAGE;

    rgFileStructure.ItemIndex := 2;

   end;

  end else

  begin

   OldFileStruct

   := UpperCase(FtpFileStructStr[NOREC]);

   CsShopper1.FileStruct := NOREC;

   rgFileStructure.ItemIndex := 0;

  end;

 finally

  Reg.CloseKey;

 end;

 try

  Reg.OpenKey(FtpClientKey, TRUE);

  if Reg.ValueExists('LastProfileUsed') then

   LastProfileUsed

   := Reg.ReadInteger('LastProfileUsed')

  else

   LastProfileUsed := 0;

 finally

  Reg.CloseKey;

 end;

 try

  Reg.OpenKey(FtpClientKey, TRUE);

  if Reg.ValueExists('NoProfiles') then

   NoProfiles := Reg.ReadInteger('NoProfiles')

  else

   NoProfiles := 1;

 finally

  Reg.CloseKey;

 end;

// Список профилей

 for Count := 0 to NoProfiles - 1 do

 begin

  ProfileName := Concat('ProfileName',

  IntToStr(Count));

  try

   Reg.OpenKey(FtpClientKey + '\Profiles' + '\

   ' + ProfileName, TRUE);

   if Reg.ValueExists('ProfileName') then

    ProfileNameList.Add(Reg.ReadString

    ('ProfileName'))

   else

    ProfileNameList.Add('PROFILE');

   OldProfiles.Add(Reg.ReadString('ProfileName'));

   if Reg.ValueExists('Host') then

    HostNameList.Add(Reg.ReadString('Host'))

   else

    HostNameList.Add('HOST');

   if Reg.ValueExists('User') then

    UserNameList.Add(Reg.ReadString('User'))

   else

    UserNameList.Add('ANONYMOUS');

   if Reg.ValueExists('Password') then

    PasswordList.Add(Reg.ReadString('Password'))

   else

    PasswordList.Add('GUEST');

   if Reg.ValueExists('RemoteDir') then

    RemoteDirList.Add(Reg.ReadString('RemoteDir'))

   else

    RemoteDirList.Add('\');

   if Reg.ValueExists('LocalDir') then

    LocalDirList.Add('LocalDir')

   else

    LocalDirList.Add('\');

  finally

   Reg.CloseKey;

  end;

 end; // цикл for

 Reg.Free;

 lbPrList.Items      := ProfileNameList;

 lbPrList.ItemIndex  := LastProfileUsed;

 edPrName.Text

 := ProfileNameList.Strings[lbPrList.ItemIndex];

 edPrHostName.Text

 := HostNameList.Strings[lbPrList.ItemIndex];

 edPrUserName.Text

 := UserNameList.Strings[lbPrList.ItemIndex];

 edPrPassword.Text

 := PasswordList.Strings[lbPrList.ItemIndex];

 edPrRemDir.Text

 := RemoteDirList.Strings[lbPrList.ItemIndex];

 edPrLocDir.Text

 := LocalDirList.Strings[lbPrList.ItemIndex];

 CsShopper1.UserName := edPrUserName.Text;

 CsShopper1.Password := edPrPassword.Text;

 lbPrList.Refresh;

end;



procedure TfrmMain.SaveProfiles;

var

 Reg : TRegistry;

 Count : Integer;

 ProfileName : String;

begin

 Reg := TRegistry.Create;

 try

  Reg.OpenKey(FtpClientKey, TRUE);

  Reg.WriteInteger('LastProfileUsed',

  LastProfileUsed);

 finally

  Reg.CloseKey;

 end;

 NoProfiles := lbPrList.Items.Count;

 try

  Reg.OpenKey(FtpClientKey, TRUE);

  Reg.WriteInteger('NoProfiles',NoProfiles);

 finally

  Reg.CloseKey;

 end;

 for Count := 0 to NoProfiles - 1 do

 begin

  ProfileName := Concat('ProfileName',

  IntToStr(Count));

  try

   Reg.OpenKey(FtpClientKey + '\Profiles' + '\

   ' + ProfileName, TRUE);

   Reg.WriteString('ProfileName',

   lbPrList.Items.Strings[Count]);

   Reg.WriteString('ProfileName',

   ProfileNameList.Strings[Count]);

   Reg.WriteString('Host',

   HostNameList.Strings[Count]);

   Reg.WriteString('User',

   UserNameList.Strings[Count]);

   Reg.WriteString('Password',

   PasswordList.Strings[Count]);

   Reg.WriteString('RemoteDir',

   RemoteDirList.Strings[Count]);

   Reg.WriteString('LocalDir',

   LocalDirList.Strings[Count]);

  finally

   Reg.CloseKey;

  end;

 end;

 Reg.Free;

end;





procedure TfrmMain.SaveOptions;

var

 Reg : TRegistry;

begin

 Reg := TRegistry.Create;

// Сохраняем имя пользователя по умолчанию

 try

  Reg.OpenKey(FtpClientKey, TRUE);

  Reg.WriteString('UserName', edDefUserName.Text);

 finally

  Reg.CloseKey;

 end;

// Сохраняем пароль по умолчанию

 try

  Reg.OpenKey(FtpClientKey, TRUE);

  Reg.WriteString('Password', edDefPassword.Text);

 finally

  Reg.CloseKey;

 end;

// Сохраняем локальный каталог по умолчанию

 try

  Reg.OpenKey(FtpClientKey, TRUE);

  Reg.WriteString('DefLocalDir',

  edDefLocalDir.Text);

 finally

  Reg.CloseKey;

 end;

// Сохраняем редактор, используемый по умолчанию

 try

  Reg.OpenKey(FtpClientKey, TRUE);

  Reg.WriteString('Editor', edDefTextEditor.Text);

 finally

  Reg.CloseKey;

 end;

 try

  Reg.OpenKey(FtpClientKey,TRUE);

  case rgFTPMode.ItemIndex of

   0 : Reg.WriteBool('Asynchronous',TRUE);

   1 : Reg.WriteBool('Asynchronous',FALSE);

  end;

 finally

  Reg.CloseKey;

 end;

 try

  Reg.OpenKey(FtpClientKey, TRUE);

  if cbHints.Checked then

   Reg.WriteBool('Hints',TRUE)

  else

   Reg.WriteBool('Hints',FALSE);

 finally

  Reg.CloseKey;

 end;

 try

  Reg.OpenKey(FtpClientKey,TRUE);

  case rgTransfer.ItemIndex of

   0 :Reg.WriteString('DTransferMode',

   FtpTransferStr[STREAM]);

   1 :Reg.WriteString('DTransferMode',

   FtpTransferStr[BLOCK]);

   2 :Reg.WriteString('DTransferMode',

   FtpTransferStr[COMPRESSED]);

  end;

 finally

  Reg.CloseKey;

 end;

 try

  Reg.OpenKey(FtpClientKey,TRUE);

  case rgFileStructure.ItemIndex of

   0 :Reg.WriteString('DFileStructure',

   FtpFileStructStr[NOREC]);

   1 :Reg.WriteString('DFileStructure',

   FtpFileStructStr[REC]);

   2 :Reg.WriteString('DFileStructure',

   FtpFileStructStr[PAGE]);

  end;

 finally

  Reg.CloseKey;

 end;

 Reg.Free;

end;



procedure TfrmMain.bbtnConnectClick(Sender:

TObject);

begin

 if (not UsedQFtp) and (not UsedProfile) then

 begin

  with CsShopper1 do

  begin

   HostName := HomeServer;

   if Status = Success then

    Start;

  end;

 end else

 if UsedQFtp then

  CsShopper1.Start

 else

 if UsedProfile then

 begin

  with CsShopper1 do

  begin

   UserName := edPrUserName.Text;

   Password := edPrPassword.Text;

   RemoteDir:= edPrRemDir.Text;

   LocalDir := edPrLocDir.Text;

   EditName := edDefTextEditor.Text;

   HostName := edPrHostName.Text;

   if Status = Success then

    Start;

  end;

 end;

end;



procedure TfrmMain.FormCreate(Sender: TObject);

begin

 bbtnQuit.Enabled       := FALSE;

 bbtnRefresh.Enabled    := FALSE;

 bbtnViewFile.Enabled   := FALSE;

 bbtnFtpCmds.Enabled    := FALSE;

 bbtnAbort.Enabled      := FALSE;

 rgFileType.Enabled     := FALSE;

 gbMoreActions.Visible  := FALSE;

 pbDataTransfer.Visible := FALSE;

 sbbtnRetr.Enabled      := FALSE;

 sbbtnStor.Enabled      := FALSE;

 OldProfiles            := TStringList.Create;

 ProfileNameList        := TStringList.Create;

 HostNameList           := TStringList.Create;

 UserNameList           := TStringList.Create;

 PasswordList           := TStringList.Create;

 RemoteDirList          := TStringList.Create;

 LocalDirList           := TStringList.Create;

 LoadSettings;

 if CsShopper1.Asynchronous then

 begin

  sbStatus.Panels[2].Text := Concat('Mode : ',

  'Asynchronous');

  rgFTPMode.ItemIndex := 0;

 end

 else

 begin

  sbStatus.Panels[2].Text := Concat('Mode : ',

  'Non-Asynchronous');

  rgFTPMode.ItemIndex := 1;

 end;

 sbStatus.Panels[0].Text

 := Concat('Local Host : ',

  CsShopper1.LocalName);

 sbStatus.Panels[3].Text

 := Concat('Status : ', 'Idle');

 pcShopper.ActivePage := tsProfiles;

 UpDate;

end;



procedure TfrmMain.bbtnFtpCmdsClick(Sender:

TObject);

begin

 gbMoreActions.Visible

 := not gbMoreActions.Visible;

 if gbMoreActions.Visible then

 begin

  bbtnFtpCmds.Hint    := 'Click here to

  close the panel of FTP commands';

  bbtnFtpCmds.Caption := 'Close';

 end

 else

 begin

  bbtnFtpCmds.Hint    := 'Click here to

  get more FTP commands';

  bbtnFtpCmds.Caption := 'FTP Cmds';

 end;

end;



procedure TfrmMain.CsShopper1Info(Sender: TObject;

Msg: String);

begin

 memLog.Lines.Add(Msg);

end;



procedure TfrmMain.CsShopper1UpDateList(Sender:

TObject;

  List: TStringList);

begin

 LbRemoteFiles.Items := List;

 lbRemoteFiles.UpDate;

 gbRemote.Caption := Concat('Files on ',

 CsShopper1.HostName);

 sbStatus.Panels[1].Text := Concat('Remote Host :

 ',CsShopper1.HostName);

end;



procedure TfrmMain.lbRemoteFilesDblClick

(Sender: TObject);

begin

 pbDataTransfer.Visible := TRUE;

 if lbRemoteFiles.ItemIndex <> -1 then

  CsShopper1.Get := lbRemoteFiles.Items.Strings

  [lbRemoteFiles.ItemIndex]

 else

 pbDataTransfer.Visible := FALSE;

end;



procedure TfrmMain.CsShopper1List

(Sender: TObject; List: TStringList);

begin

 lbRemoteFiles.Clear;

 lbRemoteFiles.Items := List;

 lbRemoteFiles.UpDate;

 gbRemote.Caption := CsShopper1.RemoteDir;

end;



procedure TfrmMain.bbtnSiteClick(Sender: TObject);

begin

 CsShopper1.SiteFtp;

end;



procedure TfrmMain.bbtnFTPHelpClick(Sender:

TObject);

var

 Counter : Integer;

begin

 frmHelp := TfrmHelp.Create(Application);

 for Counter := SFtpUser to SFtpNoop do

  frmHelp.lbHelpFtpCmds.Items.Add

  (LoadStr(Counter));

 frmHelp.ShowModal;

 CsShopper1.FtpHelp := HelpCmd;

 HelpFtpCmdList.Free;

 frmHelp.Free;

end;



procedure TfrmMain.CsShopper1Busy

(Sender: TObject; BusyFlag: Boolean);

begin

 if BusyFlag then

 begin

  lbRemoteFiles.Enabled := FALSE;

  sbStatus.Panels[3].Text

  := Concat('Status : ','Busy');

 end else

 begin

  lbRemoteFiles.Enabled := TRUE;

  sbStatus.Panels[3].Text

  := Concat('Status : ','Idle');

 end;

 Update;

end;



procedure TfrmMain.CsShopper1Progress

(Sender: TObject; Position: Integer);

begin

 pbDataTransfer.Position := Position;

 pbDataTransfer.UpDate;

end;



procedure TfrmMain.rgFileTypeClick

(Sender: TObject);

begin

 with CsShopper1 do

  case rgFileType.ItemIndex of

   0 : FileType := ASCII;

   1 : FileType := IMAGE;

   2 : FileType := AUTO;

  end;

end;



procedure TfrmMain.CsShopper1FileType

(Sender: TObject;

  FileType: TFileTypes);

begin

 case FileType of

  ASCII : rgFileType.ItemIndex := 0;

  IMAGE : rgFileType.ItemIndex := 1;

  AUTO  : rgFileType.ItemIndex := 2;

 end;

end;



procedure TfrmMain.CsShopper1Error

(Sender: TObject; Status: TConditions;

  Msg: String);

begin

 memLog.Lines.Add(Msg);

end;



procedure TfrmMain.bbtnNewDirClick

(Sender: TObject);

begin

 frmMkNewDir := TfrmMkNewDir.Create(Application);

 frmMkNewDir.ShowModal;

 if Length(NewDirName) > 0 then

  CsShopper1.MkDirName := NewDirName;

 frmMkNewDir.Free;

end;



procedure TfrmMain.bbtnDelDirClick(Sender:

TObject);

begin

 if lbRemoteFiles.ItemIndex <> -1 then

  CsShopper1.RmDirName :=



emoteFiles.Items.Strings[lbRemoteFiles.ItemIndex];

 CsShopper1.FilesList;

end;

procedure TfrmMain.CsShopper1Connect(Sender:

TObject; sSocket: Integer);

begin

 bbtnQuit.Enabled        := TRUE;

 bbtnRefresh.Enabled     := TRUE;

 bbtnViewFile.Enabled    := TRUE;

 bbtnFtpCmds.Enabled     := TRUE;

 rgFileType.Enabled      := TRUE;

 if rgFTPMode.ItemIndex = 1 then

 begin

  sbbtnRetr.Enabled       := TRUE;

  sbbtnStor.Enabled       := TRUE;

 end

 else

 begin

  sbbtnRetr.Enabled       := FALSE;

  sbbtnStor.Enabled       := FALSE;

 end;

 bbtnConnect.Enabled     := FALSE;

 bbtnExit.Enabled        := FALSE;

 rgFTPMode.Enabled       := FALSE;

 gbRemote.Caption

 := 'Remote : ' + CsShopper1.RemoteDir;

 sbStatus.Panels[1].Text

 := 'Remote Host : ' + CsShopper1.HostName;

 sbStatus.Panels[3].Text := 'Status : Connected';

 Update;

end;



procedure TfrmMain.bbtnQuitClick(Sender: TObject);

begin

 bbtnQuit.Enabled       := FALSE;

 bbtnRefresh.Enabled    := FALSE;

 bbtnViewFile.Enabled   := FALSE;

 bbtnFtpCmds.Enabled    := FALSE;

 bbtnAbort.Enabled      := FALSE;

 rgFileType.Enabled     := FALSE;

 sbbtnRetr.Enabled      := FALSE;

 sbbtnStor.Enabled      := FALSE;

 gbMoreActions.Visible  := FALSE;

 pbDataTransfer.Visible := FALSE;

 bbtnConnect.Enabled    := TRUE;

 bbtnExit.Enabled       := TRUE;

 rgFTPMode.Enabled      := TRUE;

 with sbStatus do

 begin

  Panels[1].Text := 'Remote Host : ';

  Panels[3].Text := 'Status : Idle';

 end;

 lbRemoteFiles.Clear;

 Update;

 CsShopper1.Finish;

end;

(*

procedure TfrmMain.Exit1Click(Sender: TObject);

begin

 Close;

end;

*)

procedure TfrmMain.rgFTPModeClick(Sender:

TObject);

begin

 if rgFTPMode.ItemIndex = 0 then

 begin

  CsShopper1.Asynchronous := TRUE;

  sbStatus.Panels[2].Text := 'Mode : ' +

  'Asynchronous';

  sbbtnRetr.Enabled       := FALSE;

  sbbtnStor.Enabled       := FALSE;

 end

 else

 begin

  CsShopper1.Asynchronous := FALSE;

  sbStatus.Panels[2].Text := 'Mode : ' +

  'Non-Asynchronous';

  sbbtnRetr.Enabled       := TRUE;

  sbbtnStor.Enabled       := TRUE;

 end;

 sbStatus.Update;

end;



procedure TfrmMain.bbtnRefreshClick(Sender:

TObject);

begin

 CsShopper1.FilesList

end;



procedure TfrmMain.sbbtnRetrClick(Sender:

TObject);

begin

 pbDataTransfer.Visible := TRUE;

 bbtnAbort.Enabled      := TRUE;

 CsShopper1.MGet;

end;



procedure TfrmMain.sbbtnStorClick(Sender:

TObject);

begin

 pbDataTransfer.Visible := TRUE;

 bbtnAbort.Enabled      := TRUE;

 CsShopper1.MPut;

end;

procedure TfrmMain.CsShopper1DataDone(Sender:

TObject; Done: Boolean);

begin

 if Done then

 begin

  pbDataTransfer.Visible := FALSE;

  bbtnAbort.Enabled      := FALSE

 end

 else

 begin

  pbDataTransfer.Visible := TRUE;

  bbtnAbort.Enabled      := TRUE

 end;

 pbDataTransfer.Update;

end;



procedure TfrmMain.bbtnStatClick(Sender: TObject);

begin

 CsShopper1.Stat;

end;



procedure TfrmMain.bbtnRestartClick(Sender:

TObject);

begin

 ShowMessage('Not implemented in this version');

end;



procedure TfrmMain.flbLocalDblClick(Sender:

TObject);

begin

 pbDataTransfer.Visible := TRUE;

 if flbLocal.ItemIndex <> -1 then

  CsShopper1.Put

  := flbLocal.Items.Strings[flbLocal.ItemIndex]

 else

 pbDataTransfer.Visible := FALSE;

end;



procedure TfrmMain.lbRemoteFilesClick(Sender:

TObject);

begin

 CsShopper1.RemoteFiles.Add

 (lbRemoteFiles.Items.Strings

 [lbRemoteFiles.ItemIndex]);

end;



procedure TfrmMain.flbLocalClick(Sender: TObject);

begin

 CsShopper1.LocalFiles.Add

 (flbLocal.Items.Strings[flbLocal.ItemIndex]);

end;



procedure TfrmMain.lbPrListDblClick(Sender:

TObject);

begin

 UsedProfile := TRUE;

 pcShopper.ActivePage := tsConnect;

 ActiveControl        := bbtnConnect;

 bbtnConnect.Click;

end;



procedure TfrmMain.bbtnConnectMouseDown(Sender:

TObject;

  Button: TMouseButton; Shift: TShiftState; X, Y:

  Integer);

begin

 if Button = mbRight then // Выполняем

 упрощенный ftp

 begin

  UsedQFtp := TRUE;

  UsedProfile := FALSE;

  frmQuickFtp := TfrmQuickFTP.Create(Application);

  frmQuickFtp.ShowModal;

  with CsShopper1 do

  begin

   UserName := frmQuickFtp.edUserName.Text;

   Password := frmQuickFtp.edPassword.Text;

   HostName := frmQuickFtp.edHostName.Text;

  end;

  frmQuickFtp.Free;

  ActiveControl := bbtnConnect;

  bbtnConnect.Click;

 end else

  UsedQFtp := FALSE;

end;



procedure TfrmMain.bbtnViewFileClick(Sender:

TObject);

begin

 if lbRemoteFiles.ItemIndex <> -1 then

  CsShopper1.View := lbRemoteFiles.Items.Strings

  [lbRemoteFiles.ItemIndex];

end;



procedure TfrmMain.bbtnAbortClick(Sender:

TObject);

begin

 CsShopper1.Abort;

 bbtnAbort.Enabled := FALSE;

end;



procedure TfrmMain.bbtnPrSaveClick(Sender:

TObject);

begin

 SaveProfiles;

end;



procedure TfrmMain.bbtnExitClick(Sender: TObject);

begin

 OldProfiles.Free;

 ProfileNameList.Free;

 HostNameList.Free;

 UserNameList.Free;

 PasswordList.Free;

 RemoteDirList.Free;

 LocalDirList.Free;

end;



procedure TfrmMain.lbPrListClick(Sender: TObject);

begin

 if lbPrList.ItemIndex <> -1 then

 begin

  LastProfileUsed   := lbPrList.ItemIndex;

  edPrName.Text

  := ProfileNameList.Strings[LastProfileUsed];

  edPrHostName.Text

  := HostNameList.Strings[LastProfileUsed];

  edPrUserName.Text

  := UserNameList.Strings[LastProfileUsed];

  edPrPassword.Text

  := PasswordList.Strings[LastProfileUsed];

  edPrRemDir.Text

  := RemoteDirList.Strings[LastProfileUsed];

  edPrLocDir.Text

  := LocalDirList.Strings[LastProfileUsed];

  Update;

 end;

end;



procedure TfrmMain.bbtnPrNewClick(Sender:

TObject);

begin

 NewProfile          := TRUE;

 edPrName.Text       := '';

 edPrHostName.Text   := '';

 edPrUserName.Text   := edDefUserName.Text;

 edPrPassword.Text   := edDefPassword.Text;

 edPrLocDir.Text     := edDefLocalDir.Text;

 edPrRemDir.Text     := '\';

 lbPrList.Visible    := FALSE;

end;



procedure TfrmMain.bbtnAddNewClick(Sender:

TObject);

begin

 ProfileNameList.Add(edPrName.Text);

 HostNameList.Add(edPrHostName.Text);

 UserNameList.Add(edPrUserName.Text);

 PasswordList.Add(edPrPassword.Text);

 RemoteDirList.Add(edPrRemDir.Text);

 LocalDirList.Add(edPrLocDir.Text);

 lbPrList.Items.Add(edPrName.Text);

 lbPrList.Visible := TRUE;

 lbPrList.refresh;

 NewProfile := FALSE;

end;



procedure TfrmMain.edPrNameExit(Sender:

TObject);

begin

 if (edPrName.Modified) and (not NewProfile)

 then

 begin

  lbPrList.Items.Strings[lbPrList.ItemIndex]

  := edPrName.Text;

  lbPrList.Refresh;

  ProfileNameList.Strings[lbPrList.ItemIndex]

  := edPrName.Text;

 end;

end;



procedure TfrmMain.edPrHostNameExit(Sender:

TObject);

begin

 if (edPrHostName.Modified) and (not NewProfile)

 then

  HostNameList.Strings[lbPrList.ItemIndex]

  := edPrHostName.Text;

end;



procedure TfrmMain.edPrUserNameExit(Sender:

TObject);

begin

 if (edPrUserName.Modified) and (not NewProfile)

 then

  UserNameList.Strings[lbPrList.ItemIndex]

  := edPrUserName.Text;

end;



procedure TfrmMain.edPrPasswordExit(Sender:

TObject);

begin

 if (edPrPassword.Modified) and (not NewProfile)

 then

  PasswordList.Strings[lbPrList.ItemIndex]

  := edPrPassword.Text;

end;



procedure TfrmMain.edPrRemDirExit(Sender: TObject);

begin

 if (edPrRemDir.Modified) and (not NewProfile) then

  RemoteDirList.Strings[lbPrList.ItemIndex]

  := edPrRemDir.Text;

end;



procedure TfrmMain.edPrLocDirExit(Sender: TObject);

begin

 if (edPrLocDir.Modified) and (not NewProfile)

  then

  LocalDirList.Strings[lbPrList.ItemIndex]

  := edPrLocDir.Text;

end;



procedure TfrmMain.bbtnPrDeleteClick(Sender:

TObject);

var

 Reg : TRegistry;

 Profile : String;

begin

 Reg := TRegistry.Create;

 Profile := Concat('ProfileName',IntToStr

 (lbPrList.ItemIndex));

 if Reg.DeleteKey(FtpClientKey + '\Profiles\' +

 Profile) then

 begin

  ProfileNameList.Delete(lbPrList.ItemIndex);

  HostNameList.Delete(lbPrList.ItemIndex);

  UserNameList.Delete(lbPrList.ItemIndex);

  PasswordList.Delete(lbPrList.ItemIndex);

  RemoteDirList.Delete(lbPrList.ItemIndex);

  LocalDirList.Delete(lbPrList.ItemIndex);

  lbPrList.Items.Delete(lbPrList.ItemIndex);

  edPrName.Clear;

  edPrHostName.Clear;

  edPrUserName.Clear;

  edPrRemDir.Clear;

  edPrLocDir.Clear;

  NoProfiles := lbPrList.Items.Count;

  lbPrList.Refresh;

 end;

 Reg.Free;

end;



procedure TfrmMain.bbtnLocateDefLocalDirClick

(Sender: TObject);

begin

 frmLocateDir := TfrmLocateDir.Create(Application);

 frmLocateDir.ShowModal;

 edDefLocalDir.Text := frmLocateDir.LocateDir;

 frmLocateDir.Free;

end;



procedure TfrmMain.bbtnLocateTxtEditorClick(Sender:

TObject);

begin

 frmLocateEditor := TfrmLocateEditor.Create

 (Application);

 frmLocateEditor.ShowModal;

 edDefTextEditor.Text

 := frmLocateEditor.EditorPath;

 frmLocateEditor.Free;

end;



procedure TfrmMain.BitBtn2Click(Sender:

TObject);

begin

 SaveOptions;

end;



end.

Не забудьте предварительно включить CsSocket и CsShopper в палитру компонентов. Поместите компонент CsShopper на главную форму. Создайте на форме кнопку для каждой команды FTP. Например, кнопка Connect вызывает процедуру


CsShopper1.Start:

procedure TfrmMain.bbtnConnectClick(Sender: TObject); begin
if (not UsedQFtp) and (not UsedProfile) then begin
with CsShopper1 do
begin
HostName := HomeServer;
if Status = Success then
Start;
end;
end else
if UsedQFtp then
CsShopper1.Start
else
if UsedProfile then
begin
with CsShopper1 do
begin
UserName := edPrUserName.Text;
Password := edPrPassword.Text;
RemoteDir:= edPrRemDir.Text;
LocalDir := edPrLocDir.Text;
EditName := edDefTextEditor.Text;
HostName := edPrHostName.Text;
if Status = Success then
Start;
end;
end;
end;

Профили SHOPPER32

Перед тем как подключаться к FTP-серверу с помощью программы SHOPPER32, вы должны создать на вкладке Profiles некий «профиль», включающий имя FTP-сервера, а также пользовательское имя и пароль для регистрации (см. рис. 6.4).

Профили сохраняются в системном реестре Windows и извлекаются из него перед регистрацией, чтобы вам не пришлось всякий раз вводить информацию для доступа к FTP-серверу.

Чтобы добавить новый профиль, нажмите кнопку New; при этом стирается содержимое всех текстовых полей на вкладке Profiles. Затем введите имя профиля, имя FTP-сервера, имя пользователя и пароль в текстовых полях edPrName, edPrHostName, edPrUserName и edPrPassword соответственно. Для анонимной регистрации следует ввести в поле edPrUserName строку anonymous, а в поле edPrPassword — ваш адрес электронной почты.

Рис. 6.4. Типичный вид профиля на вкладке Profiles

Нажмите кнопку Add, чтобы внести профиль в список, и затем сохраните новые данные в реестре кнопкой Save. Если потребуется удалить профиль из реестра, выделите его имя в списке Profiles и нажмите кнопку Delete. Чтобы подключиться к FTP-серверу, щелкните на имени профиля в списке Profiles, перейдите на вкладку Connect и нажмите кнопку Connect. Существует и другой, более удобный способ — дважды щелкнуть на имени профиля в списке. При этом автоматически активизируется вкладка Connect, и на ней нажимается кнопка Connect, как показано в следующем фрагменте обработчика события OnDblClick для списка


lbPrList:

procedure TfrmMain.lbPrListDblClick(Sender: TObject);
begin
UsedProfile := TRUE;
pcShopper.ActivePage := tsConnect; ActiveControl := bbtnConnect; bbtnConnect.Click;
end;

Чтобы процесс регистрации стал еще проще, мы сохраняем информацию о локальном и удаленном каталогах в текстовых полях edPrLocDir и edPrRemDir соответственно. CsShopper пользуется этой информацией для автоматиче ского, не требующего вмешательства пользователя, перехода к нужному каталогу.

Чтобы обратиться к редко используемому FTP-серверу, для которого нет смысла заводить специальный профиль, активизируйте кнопку Connect (на вкладке Connect) и щелкните на ней правой кнопкой мыши — на экране появится диалоговое окно Quick FTP. В нем следует ввести имя пользователя и пароль. Значения по умолчанию берутся с вкладки Options. Если они окажутся подходящими, вы сразу же начинаете сеанс работы кнопкой OK.

Замечание

Для получения доступа к некоторым FTP-серверам и выполнения некоторых FTP-команд (например, удаления каталога командой RMD) необходимо ввести информацию об используемом ресурсе (она посылается серверу командой ACCT). Если вы хотите работать с таким сервером, придется добавить на вкладку Profiles дополнительное текстовое поле и изменить компонент CsShopper для посылки команды ACCT с соответствующей информацией.

Подключение

Пользуясь введенной информацией, метод CsShopper.Start вызывает GetHost, чтобы открыть соединение с удаленным хостом. Если вызов функции завершится неудачно, WSAErrorMsg отображает возможную причину неудачи и присваивает Status значение Failure. В противном случае Status присваивается значение Success. При успешной установке соединения CsShopper вызывает процедуру события ConnEvent (унаследованную от CsSocket), чтобы сообщить SHOPPER32 о необходимости изменения состояния кнопок. Например, кнопка Quit блокируется до момента установления соединения, а затем становится доступной. Start вызывает FTPCommand для посылки команд USER, PASS, SYST и PWD (именно в таком порядке) с соответствующими аргументами. Затем Start устанавливает соединение данных (data connection) для пересылки списка каталогов и файлов удаленного хоста, при этом порт данных для соединения задается функцией GetPort.

Чтобы получить список каталогов, Start посылает команду LIST с помощью FTPCommand. Результат сохраняется, а последующий вызов Decode анализирует полученные данные и ищет в них информацию о каталогах и файлах.

Замечание

Механизм анализа несложен, однако описание каталогов и файлов на разных системах может выглядеть по-разному. Анализатор CsShopper работает с серверами, использующими Unix и Unix-подобные системы. Для других операционных систем он иногда выдает неверную информацию о каталогах.

Decode сравнивает первый символ каждой строки файла FTPFILE.TMP с «d» (для каталогов) или два начальных символа — с «-r» (для файлов). Если будет найден символ «d», Decode удаляет его, проверяет оставшуюся часть строки и преобразует ее в знакомый формат \ddd. Обратная косая черта сообщает SHOPPER32 о том, что строка содержит имя каталога. Аналогично в случае файлов Decode удаляет символы «-r» и ищет в строке имя, время, дату и размер файла, выделяя их в подстроки. Затем эти составные части переставляются так, чтобы получившаяся строка подходила для просмотра в окне списка SHOPPER32 (см. рис. 6.5).

Метод FRemFiles.Add, используемый внутри Decode, читает каждую сформатированную строку и заносит ее в FRemFiles. Свойство FRemFiles представляет собой список строк, производный от класса TStringList и созданный в конструкторе TCsShopper.Create.

После того как процедура Decode завершит построение списка, CsShopper передает FRemFiles процедуре TCsShopper.ChangeList, вызывающей обработчик


 OnList:

procedure TCsShopper.ChangeList(List : TStringList);
begin
if Assigned(FUpDateList) then
FUpDateList(Self, List);
end;

Рис. 6.5. Отображение файлов и каталогов в SHOPPER32

Обработчик события OnList в программе SHOPPER32 обновляет содержимое списка lbRemoteFiles:

procedure TfrmMain.CsShopper1List(Sender: TObject; List:TStringList);
begin
lbRemoteFiles.Items := List;
lbRemoteFiles.UpDate;
gbRemote.Caption := CsShopper1.RemoteDir;
end;

Закрываем соединение

Для завершения работы с FTP-сервером необходимо лишь разорвать соединение командой QUIT. Нажатие кнопки Quit приводит к вызову CsShopper1.Finish и завершению сеанса:


procedure TfrmMain.bbtnQuitClick(Sender: TObject);

begin

 bbtnQuit.Enabled       := FALSE;

 bbtnRefresh.Enabled    := FALSE;

 bbtnViewFile.Enabled   := FALSE;

 bbtnFtpCmds.Enabled    := FALSE;

 bbtnAbort.Enabled      := FALSE;

 rgFileType.Enabled     := FALSE;

 sbbtnRetr.Enabled      := FALSE;

 sbbtnStor.Enabled      := FALSE;

 gbMoreActions.Visible  := FALSE;

 pbDataTransfer.Visible := FALSE;

 bbtnConnect.Enabled    := TRUE;

 bbtnExit.Enabled       := TRUE;

 with sbStatus do

 begin

  Panels[1].Text := 'Remote Host : ';

  Panels[3].Text := 'Status : Idle';

 end;

 lbRemoteFiles.Clear;

 CsShopper1.Finish;

 Update;

end;

Прием и передача файлов

Прием и передача могут осуществляться как по отдельности, так и пакетами, состоящими из нескольких файлов, Сначала мы рассмотрим пересылку отдельных файлов. Она начинается двойным щелчком на имени принимаемого или передаваемого файла в списке.

Ключевым моментом при этом является создание нового события. После того как вы поместите список lbRemoteFiles на вкладку Connect, создайте обработчик для его события ObDblClick на вкладке Events инспектора объектов. Это событие обрабатывается процедурой TfrmMain.lbRemoteFilesDblClick. Как показано в следующем фрагменте, в результате имя файла присваивается свойству


CsShopper.Get:

procedure TfrmMain.lbRemoteFilesDblClick(Sender: TObject); begin
pbDataTransfer.Visible := TRUE;
if lbRemoteFiles.ItemIndex <> -1 then CsShopper1.Get := lbRemoteFiles.Items.Strings [lbRemoteFiles.ItemIndex]
else
pbDataTransfer.Visible := FALSE; end;

Внутри компонента CsShopper свойство Get передает имя файла в виде параметра Name процедуре Retrieve. Чтобы обеспечить правильную пересылку и сохранение файла, SetUpFileTransfer проверяет расширение файла. Для двоичных файлов (например, EXE, DLL и ZIP) SetUpFileTransfer приказывает FTP Command выдать команду TYPE IMAGE, в результате чего сервер будет пересылать файл в виде непрерывного потока байтов. Для недвоичных файлов SetUp FileTransfer выдает команду TYPE A. После того как FTP-сервер подтвердит получение команды TYPE, SetUpFileTransfer через FTPCommand посылает команду RETR имя_файла.

Изменение каталогов для пересылки файлов

Если двойной щелчок был сделан на имени каталога (например, \DELPHI), то вместо пересылки SetUpFileTransfer вызывает ChangeDir, чтобы обработать переход к другому каталогу. ChangeDir в свою очередь вызывает процедуру FTP Command, которая посылает FTP-серверу команду CWD имя_каталога (скажем, CWD \DELPHI). Если сервер принимает команду, он возвращает код ответа 250. Затем ChangeDir посылает команду LIST (тоже через FTPCommand), чтобы обновить содержимое списка файлов хоста. Наконец, Decode заполняет список содержимым нового каталога.

Передача файлов

С точки зрения внутренней логики процесс передачи файлов похож на их прием. Свойство CsShopper.Put выполняет передачу с помощью метода PutFile. Чтобы упростить передачу файла от клиента к серверу, я создал на главной форме несколько списков, производных от компонентов с вкладки Windows 3.1 палитры: dcbLocal — от TDriveComboBox, dlbLocal — от TDirectoryListBox и flbLocal — от TFileListBox.

Все эти списки синхронизированы друг с другом. При выборе в dcbLocal другого дискового устройства немедленно изменяется содержимое dlbLocal и flb Local. Как и в случае списка lbRemoteFiles, я воспользовался вкладкой Events инспектора объектов и создал новый обработчик события OnDblClickTfrm Main.flbLocalDblClick — для двойного щелчка на имени файла в списке flbLocal. Таким образом, двойной щелчок на имени передаваемого файла вызывает TfrmMain.flbLocalDblClick, в результате чего имя файла назначается свойству CsShopper1.Put.

Пересылка нескольких файлов

Второй способ позволяет переслать сразу несколько файлов (пакет). Перед тем как начинать прием, мы выделяем файлы в списке lbRemoteFiles, щелкая на их именах. При этом в обработчике TfrmMain.lbRemoteFilesClick имена файлов заносятся в строковый список RemoteFiles. Это демонстрирует следующий фрагмент кода:


procedure TfrmMain.lbRemoteFilesClick

(Sender: TObject);

begin

CsShopper1.RemoteFiles.Add

(lbRemoteFiles.Items.Strings

[lbRemoteFiles.ItemIndex]);

end;

На рис. 6.6 видно несколько файлов, выделенных в каталоге удаленного хоста и готовых к приему. После того как будут выделены все принимаемые файлы, начинайте пересылку с помощью кнопки , расположенной вверху рядом со списком lbRemoteFiles. При этом будет вызван метод CsShopper.MGet. Соответствующий код выглядит так:


procedure TfrmMain.sbbtnRetrClick(Sender: TObject);

begin

 pbDataTransfer.Visible := TRUE;

 bbtnAbort.Enabled      := TRUE;

 CsShopper1.MGet;

end;

Рис. 6.6. Выделенные файлы готовы к пакетному приему

Однако для того, чтобы описанная схема работала, нам придется изменить два свойства списка lbRemoteFiles в инспекторе объектов: во-первых, измените значение ExtendedSelect с FALSE на TRUE, а во-вторых, измените значение MultiSelect также с FALSE на TRUE. Если теперь щелкнуть на имени файла в списке lbRemoteFiles, оно заносится в строковый список CsShopper1.RemoteFiles (относящийся к типу TStringList). Аналогично в случае пакетной передачи вам придется изменить те же два свойства для списка flbLocal.

Замечание

Учтите, что возможность пакетной пересылки отсутствует в асинхронном режиме — это обусловлено трудностями с синхронизацией файловых операций.

Асинхронная пересылка файлов

Познакомившись с протоколом FTP в блокирующем (синхронном) режиме, кратко рассмотрим работу CsShopper в асинхронном режиме. Поскольку процесс регистрации на FTP-сервере подробно описан выше, наше основное внимание будет сосредоточено на пересылке, и особенно — на асинхронном приеме файла с FTP-сервера.

Перед тем как подключаться к FTP-серверу в асинхронном режиме, следует установить переключатель Asynchronous в групповом поле FTP Mode вкладки Options. Этот переключатель управляет режимом всего соединения; после того как SHOPPER32 подключится к FTP-серверу, групповое поле FTP Mode блокируется до окончания сеанса.

Процесс выбора принимаемого файла в асинхронном режиме происходит так же, как и в блокирующем режиме; другими словами, перед вызовом Retrieve мы присваиваем имя файла свойству Get. Отличия начинаются внутри Retrieve. Определив тип файла, мы присваиваем флагу состояния FFtpCmd значение FTP_TYPEI и тем самым приказываем серверу переслать файл как непрерывный поток байтов. Команда TYPE передается через процедуру SendFtpCmd.

Когда Winsock получает событие сокета FD_READ, которое происходит в результате ответа FTP-сервера на команду TYPE, он посылает процедуре FtpEvent сообщение с описанием события. В FtpEvent сообщение анализируется на предмет поиска событий FD_READ, FD_WRITE и FD_CLOSE. Для распознавания события сокета используется оператор case.

При получении события FD_READ процедура InfoEvent отправляет все содержимое буфера FRcvBuffer для вывода в приложении SHOPPER32. В буфере FRcv Buffer, содержащем код ответа от сервера, ищется символ 4 или 5, свидетель ствующий об ошибке FTP. Если поиск окажется успешным, FFtpCmd присваивается значение FTP_FAIL, которое сигнализирует приложению о возникнове нии ошибки.

В противном случае процедура ProcessRecvData обрабатывает FRcvBuffer и флаг состояния FFtpCmd с использованием оператора case. Так как FFtpCmd имеет значение FTP_TYPEI, ProcessRecvData вызывает процедуру ProcessTypeI, в которой выполняется подробный анализ содержимого FRcvBuffer. Следующий фрагмент кода показывает, как это делается:


procedure TCsShopper.ProcessTypeI;

begin

 case GetReplyCode(FRcvBuffer) of

  200 : begin

         if Pos('200-',String(FRcvBuffer)) = 0

then // Сервер ждет, пока мы создадим

// соединение данных и пошлем команду USER

         begin

          ProcessPort;

         end;

{ остаток кода пропущен }

  end; // case

  FillChar(FRcvBuffer, SizeOf(FRcvBuffer),#0);

end;

Если код ответа равен 200, вызывается процедура ProcessPort, из которой в свою очередь вызывается InitDataConn, выполняющая четыре задачи:

создание сокета для соединения данных;
вызов WSAAsyncSelect для создания лог и номера окна, позволяю щего FtpDataEvent перехватывать события сокета, связанные с соедине нием данных;
вызов функции Winsock API bind для св я нового сокета данных;
вызов listen для перевода сокета данн ы состояние «прослушивания» (listening).

Если в результате вызова InitDataConn будет создан допустимый сокет данных, ProcessPort создает для соединения данных уникальный номер порта, который затем передается процедурой SendFtpCmd. Наконец, флагу состояния FFtpCmd присваивается значение FTP_RETR, которое сигнализирует CsShopper о том, что следующее событие сокета FD_READ должно анализироваться в контекс те приема файла.

Когда на управляющем соединении происходит следующее событие FD_READ (при условии отсутствия ошибок сокета или отрицательных кодов ответа), вызывается процедура ProcessRecvData, которая в свою очередь инициирует ProcessGet.

В ProcessGet при получении кода ответа 200 (признак успеха) создается локальный файл, имя которого совпадает с именем файла на сервере. В дальнейшем код ответа 150 сигнализирует FTP-клиенту о том, что сервер приступил к пересылке информации через соединение данных.

Сразу же после того, как FTP-сервер свяжется с клиентом через соединение данных, Winsock уведомляет об этом процедуру FtpDataEvent с помощью события FD_ACCEPT. В ветви FD_ACCEPT оператора case вызывается функция WSAAsyncSelect, которая инициализирует сокет данных для приема только следующих событий: FD_READ, FD_WRITE и FD_CLOSE. Следующий фрагмент процедуры FtpDataEvent показывает, как это делается:


FD_ACCEPT :  begin

 FStartTime := GetTickCount;

 FIntTime   := FStartTime;

 if FListenSocket <> INVALID_SOCKET then

 begin

  nLen := SizeOf(TSockAddr);

  FDataSocket := accept(FListenSocket,

                        @FRemoteHost, @nLen);

  if FDataSocket = SOCKET_ERROR then

  begin

   InfoEvent(Concat('Error : ',WSAErrorMsg));

   FFtpCmd := FTP_FAIL;

   Exit;

  end;

nStat := WSAAsyncSelect(FDataSocket, FDataWnd,

                        DATA_EVENT,

                        FD_READ or

                        FD_WRITE or

                        FD_CLOSE);

if nStat = SOCKET_ERROR then

  begin

   InfoEvent(Concat('Error : ',WSAErrorMsg));

   FFtpCmd := FTP_FAIL;

   Exit;

  end;

 { остаток кода пропущен }

end;

end;

При приеме первого и последнего пакета данных через соединение данных Winsock уведомляет FtpDataEvent с помощью события FD_READ, что приводит к вызову RecvData для получения и сохранения поступающих данных в локальном файле. После завершения пересылки FTP-сервер закрывает соединение данных со своей стороны, заставляя Winsock послать сообщение FD_CLOSE. На этом пересылку файла логично было бы завершить, но иногда в сокете данных FTP-клиента все еще остаются непрочитанные данные. Чтобы избежать потерь информации, мы присваиваем флагу FTransferDone значение TRUE. Все сказанное демонстрируется следующим фрагментом кода из процедуры FtpDataEvent:


FD_CLOSE   : begin

                 FTransferDone := TRUE;

                 case FFTPCmd of

                  FTP_RETR,

                  FTP_LIST,

                  FTP_VIEW : RecvData;

                  FTP_STOR : SendData;

                 end;

                end; 

Флаг FTransferDone сообщает о необходимости продолжить чтение оставшихся данных сокета в цикле while, как показано в следующем фрагменте кода процедуры RecvData:


FTP_RETR : begin

  { часть кода пропущена }

  if FTransferDone then // Работа с

  //FTP-сервером закончена,

// однако необходимо прочитать

// и сохранить данные, оставшиеся

// в сокете данных

  begin

   Done := FALSE;

   while not Done do

   begin

    BlockWrite(FRetrFile, FDataBuffer, Response);

    { часть кода пропущена }

    Response := recv(FDataSocket, FDataBuffer,

                     SizeOf(FDataBuffer), 0);

    if Response = SOCKET_ERROR then

    begin

      Done := TRUE;

WSAAsyncSelect(FDataSocket,

// Прекратить посылку

               FDataWnd, 0, 0); // уведомлений

      CloseSocket(FDataSocket);

      System.CloseFile(FRetrFile);

      ChangeBusy(FALSE);

      ChangeDataDone(TRUE);

      InfoEvent(Concat('ERROR : ',WSAErrorMsg));

    end;

    if Response = 0 then   // Данных не осталось

    begin

      { часть кода пропущена }

      Done := TRUE;

      WSAAsyncSelect(FDataSocket,

      FDataWnd, 0, 0);

      CloseSocket(FDataSocket);

      System.CloseFile(FRetrFile);

      ChangeBusy(FALSE);

      ChangeDataDone(TRUE);

      GetList;

    end;

  end;

end else

if Response > 0 then

// FTP-сервер продолжает

// посылать данные,

// их необходимо обработать

begin

  BlockWrite(FRetrFile, FDataBuffer, Response);

  { часть кода пропущена }

end;

end;

Передача файла FTP-серверу в асинхронном режиме выполняется по тому же принципу, что и прием.

Положи на место!

В асинхронном режиме в отличие от блокирующего можно легко прервать затянувшуюся пересылку файла — достаточно нажать кнопку Abort на вкладке Connect (обратите внимание на то, что в блокирующем режиме эта кнопка недоступна). При нажатии кнопки Abort вызывается метод CsShopper.Abort, который посылает серверу через управляющее соединение команду ABOR. Рассмотрим следующий фрагмент кода:


procedure TCsShopper.Abort;

begin

  ChangeBusy(TRUE);

  SendFtpCmd(LoadStr(SFtpAbor));

  FFtpCmd := FTP_ABORT;

  ChangeBusy(FALSE);

end;

При получении кода ответа 226, означающего успешную отмену пересылки, CsShopper.ProcessAbort закрывает соединение данных, а в случае приема файла — стирает локальный файл.

Заключение

FTP-клиент CsShopper — невизуальный компонент. Он не умеет сохранять и загружать имена хостов, имена пользователей, пароли и сведения о ресурсах. Все это остается на совести программистов, которые должны спроектировать эти визуальные средства в соответствии с потребностями конкретного приложения. Тем не менее приложение SHOPPER32 наглядно показывает, как легко можно при необходимости организовать сохранение и загрузку профилей.

 

Используются технологии uCoz