уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

ftp_clientsПрошло уже больше месяца с момента последней публикации в блоге. Так получилось, что все это время я к Delphi практически не обращался — занимался вещами далекими и от Delphi и от программирования, в принципе. По сему пропустил и выход AppMethod, но, думаю, что этот пробел я со временем восполню, а пока решил вернуться к работе над Synapse и предоставить на всеобщий обзор следующую главу, касающуюся работы с FTP в Synapse. По этому протоколу в блоге информации мало, поэтому большая часть главы писалась с нуля. PDF-версия главы — в конце сообщения.  Первая глава по сокетам в Synapse доступна здесь, вторая (по HTTP) — здесь.

Общие сведения о протоколе

FTP (англ. File Transfer Protocol — протокол передачи файлов) — стандартный протокол, предназначенный для передачи файлов по TCP-сетям (например, Интернет).

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

Можно использовать протокол SSH для безопасной передачи, скрывающей (шифрующей) логин и пароль, а также шифрующей содержимое.

FTP является одним из старейших прикладных протоколов, появившимся задолго до HTTP, и даже до TCP/IP, в 1971 году.

Особенность протокола FTP состоит в том, что он использует множественное  подключение. При этом один канал является управляющим, через который поступают команды серверу и возвращаются его ответы (обычно через TCP-порт 21), а через остальные происходит собственно передача данных, по одному каналу на каждую передачу.

FTP

Поэтому в рамках одной сессии по протоколу FTP можно передавать одновременно несколько файлов, причём в обоих направлениях. Для каждого канала данных открывается свой TCP порт, номер которого выбирается либо сервером, либо клиентом, в зависимости от режима передачи.

Передача данных может осуществляться в любом из трёх режимов:

  1. Поточный режим — данные посылаются в виде непрерывного потока, освобождая FTP от выполнения какой бы то ни было обработки. Вместо этого, вся обработка выполняется TCP.
  2. Блочный режим — FTP разбивает данные на несколько блоков (блок заголовка, количество байт, поле данных) и затем передаёт их TCP.
  3. Режим сжатия — данные сжимаются единым алгоритмом.

При передаче данных по сети могут быть использованы четыре представления данных:

  1. ASCII — используется для текста. Данные, если необходимо, до передачи конвертируются из символьного представления на хосте-отправителе в «восьмибитный ASCII», и (опять же, если необходимо) в символьное представление принимающего хоста. Как следствие, этот режим не подходит для файлов, содержащих не только обычный текст.
  2. Режим изображения (бинарный) — устройство-отправитель посылает каждый файл байт за байтом, а получатель сохраняет поток байтов при получении. Поддержка данного режима была рекомендована для всех реализаций FTP.
  3. EBCDIC — используется для передачи обычного текста между хостами в кодировке EBCDIC. В остальном, этот режим аналогичен ASCII-режиму.
  4. Локальный режим — позволяет двум компьютерам с идентичными установками посылать данные в собственном формате без конвертации в ASCII.

FTP может работать в активном или пассивном режиме, от выбора которого зависит способ установки соединения.

В активном режиме клиент создаёт управляющее TCP-соединение с сервером и отправляет серверу свой IP-адрес и произвольный номер клиентского порта, после чего ждёт, пока сервер не запустит TCP-соединение с этим адресом и номером порта. В случае, если клиент находится за брандмауэром и не может принять входящее TCP-соединение, может быть использован пассивный режим.

В пассивном режиме клиент использует поток управления, чтобы послать серверу команду PASV, и затем получает от сервера его IP-адрес и номер порта, которые затем используются клиентом для открытия потока данных с произвольного клиентского порта к полученному адресу и порту.

Сервер отвечает по потоку управления трехзначными ASCII-кодами состояния с необязательным текстовым сообщением. Например, «200» (или «200 ОК») означает, что последняя команда была успешно выполнена. Цифры представляют код ответа, а текст — разъяснение или запрос. Текущая передача по потоку данных может быть прервана с помощью прерывающего сообщения, посылаемого по потоку управления.

Команды FTP

Список основных команд FTP представлен в таблице ниже

Команда

Описание

ABOR Прервать передачу файла
CDUP Сменить директорию на вышестоящую
CWD Сменить директорию
DELE Удалить файл (DELE filename).
EPSV Войти в расширенный пассивный режим. Применяется вместо PASV.
HELP Выводит список команд, принимаемых сервером
LIST Возвращает список файлов директории. Список передаётся через соединение данных.
MDTM Возвращает время модификации файла
MKD Создать директорию
NLST Возвращает список файлов директории в более кратком формате, чем LIST. Список передаётся через соединение данных
NOOP Пустая операция
PASV Войти в пассивный режим. Сервер вернёт адрес и порт, к которому нужно подключиться, чтобы забрать данные. Передача начнётся при введении следующих команд: RETR, LIST и т.д.
PORT Войти в активный режим. Например PORT 12,34,45,56,78,89. В отличие от пассивного режима для передачи данных сервер сам подключается к клиенту.
PWD Возвращает текущую директорию
QUIT Отключиться
REIN Реинициализировать подключение
RETR Скачать файл. Перед RETR должна быть команда PASV или PORT
RMD Удалить директорию
RNFR и RNTO Переименовать файл. RNFR — что переименовывать, RNTO — во что.
SIZE Возвращает размер файла
STOR Закачать файл. Перед STOR должна быть команда PASV или PORT.
SYST Возвращает тип системы (UNIX, WIN, …)
TYPE Установить тип передачи файла (бинарный, текстовый)
USER Имя пользователя для входа на сервер

Коды ответов FTP

Ответ FTP сервера на любую команду FTP сервера состоит из трех цифр.
Первая цифра отвечает за один из трёх исходов: успех, отказ или указание на ошибку либо неполный ответ.
Вторая цифра определяет тип ошибки.
Третья цифра окончательно специфицирует ошибку.

Рассмотрим значение каждой цифры в каждой позиции.

Первая позиция

Вторая позиция

Третья позиция

0

Не определено Синтаксическая ошибка

Окончательно специфицирует ошибку

1

Команда принята к выполнению но ещё не завершена Информационное сообщение

2

Выполнение команды успешно завершено Ошибка соединения. Сообщение относится либо к управляющему соединению, либо к соединению данных

3

Команда принята и ожидается какая-либо дополнительная команда Сообщение об аутентификации пользователя и его правах

4

В данный момент команда не может быть выполнена Не определено

5

Принципиальная невозможность выполнения команды Сообщение о состоянии файловой системы

В Synapse работа с протоколом FTP реализована в модуле FTPSend.pas. Рассмотрим этот модуль подробно и реализуем несколько примеров по работе с FTP в Synapse.

Модуль FTPSend.pas

Основным классом в модуле FTPSend.pas является TFPSend – класс, реализующий клиент для работы с FTP протоколом. Однако для упрощения работы с данными, поступающими от сервера, этот модуль содержит также и другие вспомогательные классы, которые мы и рассмотрим вначале.

Класс TFTPListRec

TFTPListRec

Объекты этого класса хранят информацию о файлах и поддиректориях, полученных с FTP-сервера.

У класса TFTPListRec определены следующие свойства:

property FileName: string
Имя файла или подкаталог
property Directory: Boolean
True, если объект является подкаталогом
property Readable: Boolean
True, если есть доступ на чтение объекта
property FileSize: int64
Размер файла в байтах
property FileTime: TDateTime
Дата/время файла. Используется часовой пояс сервера.
property OriginalLine: string
«Сырые» данные об объекте, полученные с сервера
property Mask: string
Маска, которая будет использоваться использована для разбора ответа сервера
property Permission: string
Разрешения, установленные для объекта.

 

Класс TFTPList

TFTPList

Объект этого класса содержит информацию обо всех объектах, находящихся в определенной директории на FTP-сервере.

У класса определены следующие свойства:

property List: TList
Список объектов
property Items[Index: Integer]: TFTPListRec
Запись об отдельном объекте в списке
property Lines: TStringList
«Сырые» данные, полученные от сервера
property Masks: TStringList
Перечень масок для анализа данных сервера. Значения в этом списке устанавливаются по умолчанию, однако вы можете добавлять свои собственные маски для работы анализатора
property UnparsedLines: TStringList
Список строк, анализ которых не удалось провести с использованием текущего списка масок.

Также класс содержит методы:

procedure Clear; virtual;
Удаление всех данных об объектах. Очищаются свойства List, Lines и UnparsedLines. Свойство Masks остается без изменений.
function Count: integer; virtual;
Возвращает количество элементов в списке List
procedure Assign(Value: TFTPList); virtual;
Копирует все данные из Value в текущий объект
procedure ParseLines; virtual;
Производит анализ данных, содержащихся в списке Lines, формируя при этом список объектов List.

 

Класс TFTPSend

TFTPSend

Как было сказано выше, класс TFTPSend реализует работу с протоколом FTP. Рассмотрим какие свойства, методы и события содержит этот класс.

Свойства класса:

property ResultCode: Integer
Содержит код последнего ответа сервера
property ResultString: string
Содержит строку описания последнего ответа сервера
property FullResult: TStringList
После выполнения очередной команды содержит список всех строк ответа сервера
property Account: string
Содержит информацию об аккаунте, которая может потребоваться для входа на сервер. В большинстве случаев использовать это свойство не требуется
property FWHost: string
Адрес брэндмауэра. Если это свойство не заполнено, то считается, что брэндмауэр не используется.
property FWPort: string
Порт брэндмауэра.
property FWUsername: string
Имя пользователя брэндмауэра.
property FWPassword: string
Пароль пользователя брэндмауэра.
property FWMode: integer
Тип брэндмауэра. В зависимости от значения этого свойства определяется последовательность выбора параметров при входе на сервер.
property Sock: TTCPBlockSocket
Объект сокета управляющего канала.
property DSock: TTCPBlockSocket
Объект сокета канала для передачи данных
property DataStream: TMemoryStream
Поток, используемый для передачи данных в случае, если свойство DirectFile равно False
property DataIP: string
После создания соединения для канала данных содержит IP адрес этого соединения
property DataPort: string
После создания соединения для канала данных содержит порт этого соединения
property DirectFile: Boolean
Определяет режим передачи данных. Если это свойство равно False, то передача данных осуществляется с использованием потока DataStream, иначе – данные для передачи получаются непосредственно из файла, имя которого указано в свойстве DirectFileName. Значение по умолчанию False
property DirectFileName: string
Имя файла, который используется для передачи данных
property CanResume: Boolean
После подключения к серверу указывает поддерживает ли сервер докачку файлов
property PassiveMode: Boolean
Если это свойство равно True, то работа ведется в пассивном режиме. Значение по умолчанию True.
property ForceDefaultPort: Boolean
Если свойство равно True, то для соединения канала данных устанавливается стандартный порт (20). По умолчанию это свойство равно False, т.к. сервер может назначить свой собственный порт при установке соединения. Если работа ведется в пассивном режиме, то это свойство не используется
property ForceOldPort: Boolean
Если это свойство равно True, то отсутствует возможность использовать такие команды как EPSV и EPRT. Однако без этих команд вы не сможете использовать IPv6. Использовать это свойство рекомендуется только в случае возникновения проблем при работе с брэндмауэром.
property FtpList: TFTPList
Содержит список объектов, полученных после выполнения метода List
property BinaryMode: Boolean
Если значение равно True, то работа ведется в бинарном режиме, иначе – в режиме ASCII.
property AutoTLS: Boolean
Если значение равно True, то вначале проверяется поддержка сервером SSL/TLS, а затем производится подключение в зависимости от результатов проверки.
property FullSSL: Boolean
Если сервер использует SSL/TLS, то это свойство необходимо установить в значение True
property IsTLS: Boolean
Указывает использует ли канал управления SSL/TLS
property IsDataTLS: Boolean
Указывает использует ли канал данных SSL/TLS
property TLSonData: Boolean
Если значение равно True (по умолчанию), то при передаче данных осуществляется попытка использования SSL/TLS

Методы класса

function ReadResult: Integer; virtual;
Ожидает ответ сервера и в результате возвращает его код. Этот метод следует вызывать только в особых случаях работы с FTP
procedure ParseRemote(Value: string); virtual;
Разбирает ответ сервера Value, полученный в результате выполнения команды PASV. Этот метод следует вызывать только в особых случаях работы с FTP
procedure ParseRemoteEPSV(Value: string); virtual;
Разбирает ответ сервера Value, полученный в результате выполнения команды EPSV. Этот метод следует вызывать только в особых случаях работы с FTP
function FTPCommand(const Value: string): integer; virtual;
Отправляет команду Value на сервер и возвращает код ответа. Этот метод удобно использовать для выполнения команд не предусмотренных другими методами класса.
function Login: Boolean; virtual;
Осуществляет вход на сервер и, если вход успешно выполнен, то возвращает в результате True
function Logout: Boolean; virtual;
Закрывает текущую сессию работы с FTP сервером
procedure Abort; virtual;
Прерывает текущую передачу данных
procedure TelnetAbort; virtual;
Метод аналогичен Abort, но перед прерыванием операции на сервер отправляется команда ABOR, которую требуют некоторые серверы
function List(Directory: string; NameList: Boolean): Boolean; virtual;
Получает с сервера информацию об объектах в директории Directory. Если Directory содержит пустую строку, то получается информация из текущей директории. Если параметр NameList равен True, то запрашиваются только имена объектов в директории, иначе – получается вся информация, которая затем размещается в списке FtpList.
function RetrieveFile(const FileName: string; Restore: Boolean): Boolean; virtual;
Получает данные файла FileName сервера. Если сервер поддерживает докачку и параметр Restore равен True, то получение данных возобновляется с места последнего прерывания.
function StoreFile(const FileName: string; Restore: Boolean): Boolean; virtual;
Загружает данные из файла FileName на сервер. Если сервер поддерживает докачку и параметр Restore равен True, то получение данных возобновляется с места последнего прерывания.
function StoreUniqueFile: Boolean; virtual;
Загружает данные на сервер и присваивает новому файлу уникальное имя.
function AppendFile(const FileName: string): Boolean; virtual;
Дописывает данные в файл с именем FileName на сервере
function RenameFile(const OldName, NewName: string): Boolean; virtual;
Изменяет имя файла на сервере с OldName на NewName
function DeleteFile(const FileName: string): Boolean; virtual;
Удаляет файл с именем FileName с сервера
function FileSize(const FileName: string): int64; virtual;
Возвращает размер файла FileName на сервере
function NoOp: Boolean; virtual;
Посылает пустую команду NOOP на сервер для поддержки соединения
function ChangeWorkingDir(const Directory: string): Boolean; virtual;
Изменяет текущую директорию на директорию Directory
function ChangeToParentDir: Boolean; virtual;
Изменяет текущую директорию на родительскую
function ChangeToRootDir: Boolean; virtual;
Изменяет текущую директорию на корневую
function DeleteDir(const Directory: string): Boolean; virtual;
Удаляет директорию Directory с сервера
function CreateDir(const Directory: string): Boolean; virtual;
Создает директорию Directory на сервере
function GetCurrentDir: String; virtual;
Возвращает текущую рабочую директорию
function DataRead(const DestStream: TStream): Boolean; virtual;
Устанавливает канал данных и считывает данные с сервера. Эта функция может потребоваться при передаче на сервер своих команд методом FTPCommand
function DataWrite(const SourceStream: TStream): Boolean; virtual;
Устанавливает канал данных и отправляет данные на сервер. Эта функция может потребоваться при передаче на сервер своих команд методом FTPCommand

События класса

type
TFTPStatus = procedure(Sender: TObject; Response: Boolean;
const Value: string) of object;
property OnStatus: TFTPStatus
 
Это событие возникает при передаче команд на сервер или получении ответа с сервера.
Sender – указывает на объект TFTPSend
Value – команда или ответ сервера на команду
Response – содержит True, если строка Value является ответом сервера

 

Вспомогательные методы

function FtpGetFile(const IP, Port, FileName, LocalFile,  User, Pass: string): Boolean;

Функция для получения файла с именем FileName с сервера и сохранения его на диске с именем LocalName.

function FtpPutFile(const IP, Port, FileName, LocalFile,  User, Pass: string): Boolean;

Функция для передачи файла LocalName с локального диска на сервер. Файл сохраняется на сервере с именем FileName.

function FtpInterServerTransfer(const FromIP, FromPort, FromFile, FromUser, FromPass: string; const ToIP, ToPort, ToFile, ToUser, ToPass: string): Boolean;

Функция для передачи файла с одного FTP сервера на другой.

Примеры работы с FTP в Synapse

Первый пример: подключение к серверу и работа с директориями

В качестве первого примера рассмотрим как правильно подключаться к FTP серверу и осуществлять навигацию по директориям.

Создаем новый проект (назовем его «syna_ftp») и размещаем на главной форме компоненты как показано на рисунке:

ftp_1

На форме расположены следующие компоненты:

  • 3 TEdit для ввода адреса сервера, имени пользователя и пароля
  • TButton для подключения к серверу или разрыва соединения
  • TListView для вывода информации об объектах в текущей директории
  • TMemo для вывода информации полученной/отправленной по каналу управления

Наша программа будет работать следующим образом:

  1. Подключаться к серверу и считывать данные об объектах в текущей директории
  2. Двойной клик по записи в ListView будет осуществлять переход в выбранную директорию или на один уровень вверх (для этого мы предусмотрим в ListView отдельный элемент.

Теперь подключаем в uses главного модуля приложения модуль FtpSend и пишем обработчики событий OnCreate и OnDestroy главной формы и OnStatus для объекта FTPSend:

unit uMain;
 
interface
 
uses
  ..., ftpsend, ....;
 
type
  TForm3 = class(TForm)
    ...
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    procedure Status(Sender: TObject; Response: Boolean; const Value: string);
  public
    FTPSend: TFTPSend;
  end;
 
var
  fmain: Tfmain;
 
implementation
 
{$R *.dfm}
 
procedure TForm3.FormCreate(Sender: TObject);
begin
  FTPSend:=TFTPSend.Create;
  FTPSend.OnStatus:=Status;
end;
 
procedure TForm3.FormDestroy(Sender: TObject);
begin
  FTPSend.Free;
end;
 
procedure Tfmain.Status(Sender: TObject; Response: Boolean;
  const Value: string);
begin
  if Response then
    memLog.Lines.Add('--> '+Value)
  else
    memLog.Lines.Add('<-- '+Value)
end;

Теперь напишем вспомогательную процедуру, которая будет формировать содержимое ListView по данным из свойства FtpList объекта FTPSend:

procedure Tfmain.UpdateFileList;
var LI: TlistItem;
    I: Integer;
begin
  lbCurrDir.Caption:=FTPSend.GetCurrentDir;
  ListView1.Items.BeginUpdate;
  try
   ListView1.Items.Clear;
    {создаем элемент для перехода на один уровень вверх}
    LI:=ListView1.Items.Add;
    LI.Caption:='[...]';
    {заполняем список информацией об объектах в текущей директории}
    for I := 0 to FTPSend.FtpList.Count-1 do
      begin
        LI:=ListView1.Items.Add;
        LI.Caption:=FTPSend.FtpList[i].FileName;
        if FTPSend.FtpList[i].Directory then
          Li.SubItems.Add('Папка')
        else
          Li.SubItems.Add('Файл');
        LI.SubItems.Add(IntToStr(FTPSend.FtpList[i].FileSize));
        LI.SubItems.Add((DateToStr(FTPSend.FtpList[i].FileTime)));
        Li.SubItems.Add(FTPSend.FtpList[i].Permission);
      end;
  finally
    ListView1.Items.EndUpdate;
  end;
end;

Теперь напишем обработчик события OnClick кнопки btnLogin:

procedure Tfmain.btnLoginClick(Sender: TObject);
const cLogin = 'Login';
      cLogout = 'Logout';
var i:integer;
begin
  if btnLogin.Caption=cLogin then
    begin
      //задаем параметры соединения
      FTPSend.TargetHost:=edServer.Text;
      FTPSend.UserName:=edUserName.Text;
      FTPSend.Password:=edPassword.Text;
      //используем пассивный режим
      FTPSend.PassiveMode:=True;
      //вход произведен успешно
      if FTPSend.Login then
        begin
         //выводим информацию о текущей директории
         lbCurrDir.Caption:=FTPSend.GetCurrentDir;
         //получаем информацию об объектах в директории
         FTPSend.List(lbCurrDir.Caption, false);
         //обновляем список
         UpdateFileList;
         //меняем надпись кнопки на "Logout"
         btnLogin.Caption:=cLogout
        end
      else
        memLog.Lines.Add('Error connect')
   end
  else
    begin
      //завершаем сессию
      FTPSend.Logout;
      //меняем надпись кнопки на "Login"
      btnLogin.Caption:=cLogin
    end;
end;

Остается написать обработчик двойного клика в ListView и проверить работу программы. Пишем обработчик:

procedure Tfmain.ListView1DblClick(Sender: TObject);
var b: boolean;
begin
  if not Assigned(ListView1.Selected) then Exit;
  //если пробуем подняться на уровень вверх
  if ListView1.ItemIndex=0 then
    b:=FTPSend.ChangeToParentDir
  else
    //пробуем сменить директорию на выбранную
    b:=FTPSend.ChangeWorkingDir(ListView1.Selected.Caption);
 if b then
   begin
     //смена текущей директории прошла успешно
     //получаем данные об объектах в текущей директории
     FTPSend.List(EmptyStr,False);
     //обновляем список
     UpdateFileList;
   end
 else
   memLog.Lines.Add('Change current directory filed')
end;

Теперь можно запустить приложение и проверить его работу:

ftp_2

По логу работы программы вы можете проследить всю цепочку «общения» клиента и сервера, а используя ListView перемещаться по директориям на сервере.

Второй пример: передача данных с заполнением ProgressBar’а

При изучении работы класса THTTPSend мы рассматривали пример скачивания больших файлов с заполнением ProgressBar’a. Рассмотрим и в этой главе подобный пример – научимся загружать/скачивать данные с заполнением ProgressBar.

В начале рассмотрим в чем будут заключаться особенности этого примера от рассмотренного ранее. Во-первых, если при скачивании файла по HTTP сервер может не вернуть нам размер скачиваемого файла, то при работе с FTP мы можем точно знать каков размер файла (при выполнении команды LIST мы получим всю информацию об объекте, включая и его размер). Во-вторых, следует учитывать особенность самого протокола FTP – многоканальную работу. Так, если обработчик OnStatus объекта FTPSend возвращает нам информацию исключительно из канала управления, то для отслеживания передачи информации по каналу данных нам необходимо написать обработчик OnStatus для сокета DSock (сокет канала данных).

Теперь приступим к работе над примером. В качестве основы для работы воспользуемся предыдущим примером. Копируем код предыдущего примера в другую директорию и добавляем на форму компонент TProgressBar как показано на рисунке ниже, а также компоненты:

  • TPopupMenu для выбора операции с объектом в текущей директории.
  • TOpenFileDialog – для выбора файла на локальном диске.

У компонента ListView в свойстве PopupMenu указываем компонент меню, а в самом меню создаем следующие элементы:

  1. Скачать файл
  2. Загрузить файл на сервер
  3. Удалить

Теперь приступим к написанию кода программы. Чтобы использовать события сокета нам необходимо подключить в uses модуль blcksock. Теперь напишем обработчик события OnStatus для сокета:

 ftp_3

procedure Tfmain.DSockStatus(Sender: TObject; Reason: THookSocketReason;
  const Value: String);
begin
  if Reason in [HR_ReadCount, HR_WriteCount] then
    ProgressBar1.Position:=ProgressBar1.Position+StrToInt(Value);
end;

Здесь мы проверяем какая операция в данный момент осуществляется и, если это операция чтения или записи, то увеличиваем свойство Position у ProgressBar.

Теперь напишем обработчик OnClick элемента меню «Скачать файл». Обработчик может выглядеть следующим образом:

procedure Tfmain.N1Click(Sender: TObject);
begin
  if not Assigned(ListView1.Selected) then Exit;
  if ListView1.Selected.Index=0 then Exit;
  //если выбрана папка, то выводим в лог сообщение
  if ListView1.Selected.SubItems[0]='Папка' then
    memLog.Lines.Add('Can''''t downoload directory')
  else
    //скачиваем файл
    begin
      //устанавливаем свойства ProgressBar
      ProgressBar1.Max:=StrToInt(ListView1.Selected.SubItems[1]);
      ProgressBar1.Position:=0;
      //определяем обработчик событие
      FTPSend.DSock.OnStatus:=DSockStatus;
      //пробуем скачать файл
      if FTPSend.RetrieveFile(ListView1.Selected.Caption,False) then
        begin
          //скачивание прошло успешно - сохраняем файл
          FTPSend.DataStream.SaveToFile(ListView1.Selected.Caption);
          //выводим сообщение об успешной закачке
          ShowMessage('Download complete');
        end
      else
        //загрузка не удалась - выводим сообщение в лог
        memLog.Lines.Add('Can''''t downoload file');
      ProgressBar1.Position:=0;
      FTPSend.DSock.OnStatus:=nil;
    end;
end;

Рассмотрим этот обработчик подробно.

Чтобы скачать файл мы вначале проверяем выбран ли какой-либо объект в списке ListView и, если ничего не выбрано, то выходим из обработчика.

if not Assigned(ListView1.Selected) then Exit;

Если в ListView выделен какой-либо элемент, то определяем является ли этот элемент самым первым в списке.

if ListView1.Selected.Index=0 then Exit;

Если это так, то значит пользователь выбрал служебный элемент […] для перехода на уровень вверх и, следовательно, т.к. скачать мы ничего в этом случае не можем, то работа прерывается.
Следующая проверка – проверка на тип объекта.

if ListView1.Selected.SubItems[0]='Папка' then
    memLog.Lines.Add('Can''''t downoload directory')

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

Если в ListView выбран файл, то мы точно знаем его размер – он записан во втором столбце ListView, поэтому мы можем настроить наш ProgressBar, установив ему верное значение свойство Max, что мы и делаем.

//устанавливаем свойства ProgressBar
ProgressBar1.Max:=StrToInt(ListView1.Selected.SubItems[1]);

Далее мы назначаем обработчик события OnStatus сокета канала данных:

FTPSend.DSock.OnStatus:=DSockStatus;

И пробуем скачать файл и сохранить его на диске

if FTPSend.RetrieveFile(ListView1.Selected.Caption,False) then
  begin
   //скачивание прошло успешно - сохраняем файл
   FTPSend.DataStream.SaveToFile(ListView1.Selected.Caption);
   <i>//выводим сообщение об успешной закачке
   ShowMessage('Download complete');
  end
else
 //загрузка не удалась - выводим сообщение в лог
 memLog.Lines.Add('Can''''t downoload file');</i>

В любом случае, после выполнения операции скачивания файла убираем с сокета обработчик и обнуляем прогресс в ProgressBar:

ProgressBar1.Position:=0;
FTPSend.DSock.OnStatus:=nil;

Если не снять обработчик OnStatus с сокета, то при последующей работе с FTPSend ProgressBar будет пробовать заполняться, т.к. через канал данных проходят все данные, в т.ч. и данные о содержимом директории.

Обработчик события OnClick элемента меню «Загрузить файл на сервер» будет немногим отличаться от приведенного выше, поэтому просто приведем его код с комментариями:

procedure Tfmain.N4Click(Sender: TObject);
var b:boolean;
begin
  if OpenDialog1.Execute then
    begin
      //загружаем данные файла в поток
      FTPSend.DataStream.LoadFromFile(OpenDialog1.FileName);
      //устанавливаем значения свойств ProgressBar'a
      ProgressBar1.Position:=0;
      ProgressBar1.Max:=FTPSend.DataStream.Size;
      //определяем обработчик события
      FTPSend.DSock.OnStatus:=DSockStatus;
      //пробуем загрузить файл в директорию
      b:=FTPSend.StoreFile(ExtractFileName(OpenDialog1.FileName), False);
      //убираем обработчик события
      FTPSend.DSock.OnStatus:=nil;
      if b then
        begin
          //загрузка прошла успешно - обновляем список объектов
          FTPSend.List(EmptyStr,False);
          UpdateFileList;
        end;
      //обнуляем прогресс
      ProgressBar1.Position:=0;
    end;
end;

И третий обработчик – обработчик события OnClick элемента меню «Удалить»:

procedure Tfmain.N2Click(Sender: TObject);
var b:boolean;
begin
  if not Assigned(ListView1.Selected) then Exit;
  if ListView1.Selected.Index=0 then Exit;
  if ListView1.Selected.SubItems[0]='Папка' then
    b:=FTPSend.DeleteDir(ListView1.Selected.Caption)
  else
    b:=FTPSend.DeleteFile(ListView1.Selected.Caption);
  if b then
    begin
      memLog.Lines.Add('Delete object complete');
      //получаем данные об объектах в текущей директории
      FTPSend.List(EmptyStr,False);
      //обновляем список
      UpdateFileList;
    end
  else
    memLog.Lines.Add('Can''''t delete object');
end

Теперь можно запустить приложение, подключиться к серверу и проверить работу с файлами:

ftp_4 

Третий пример: докачка файлов

Ещё одной немаловажной проблемой при работе с FTP является докачка файлов. На FTP сервере могут содержаться файлы самого разного размера и наличие возможности загрузить файл, начиная с определенной позиции после восстановления соединения с сервером, является скорее необходимостью для современных FTP-клиентов, а не «ещё одной дополнительной возможностью». И в этом примере мы разберемся с тем, как можно получать/отправлять данные с места определенной позиции  в файле или, иначе говоря, производить докачку файлов.

Как докачать файл по FTP?

Чтобы начать загрузку файла с определенной позиции FTP-клиент должен выполнить как минимум две команды:

  1. REST – для указания серверу позиции с которой необходимо получать данные из файла.
  2. RETR – для скачивания файла.

В Synapse обе эти команды реализованы в методах TFTPSend, которые мы уже использовали ранее – это RetrieveFile и StoreFile. Оба эти метода содержат такой входной параметр как Restore. Этот параметр указывает будет ли производится докачка файла с последней позиции в файле (параметр содержит True) или передача данных начнется с начала (параметр содержит False).

Так как не каждый FTP-сервер поддерживает докачку файлов, то для того, чтобы узнать поддерживается ли эта возможность на сервере, у TFTPSend имеется специальное свойство — CanResume, которое будет содержать значение True, если сервер поддерживает докачку.

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

Реализуем FTP-клиент с возможностью докачки файлов

Копируем исходный код предыдущего примера в отдельную папку и переименовываем проект, например, в syna_ftp_resume.

Теперь нам необходимо немного изменить обработчики OnClick элементов меню «Скачать файл» и «Загрузить файл на сервер». Изменения в этих методах будут минимальными, а точнее – по одной строке в каждом методе:

для обработчика OnClick у элемента «Скачать файл» строку:

if FTPSend.RetrieveFile(ListView1.Selected.Caption,False) then

необходимо поменять следующим образом:

if FTPSend.RetrieveFile(ListView1.Selected.Caption, FTPSend.CanResume) then

и, соответственно, в обработчике OnClick элемента «Загрузить файл на сервер» строка:

b:=FTPSend.StoreFile(ExtractFileName(OpenDialog1.FileName), False);

меняется на:

b:=FTPSend.StoreFile(ExtractFileName(OpenDialog1.FileName), FTPSend.CanResume);

Теперь, если FTP-сервер поддерживает докачку файлов, то, в случае разрыва связи с сервером и последующего его восстановления, файл будет скачиваться/закачиваться с той позиции на которой произошел разрыв. Но на этом наш пример не заканчивается. Не стоит забывать про такой элемент интерфейса нашей программы как ProgressBar. Если Вы прямо сейчас попробуете реализовать, например, докачку файла, начиная с середины, то заметите, что после восстановления процесса загрузки ProgressBar заполниться ровно наполовину. Происходит это потому, что мы не указываем в свойстве ProgressBar.Position начальную позицию в файле с который мы начинаем загрузку. Чтобы ProgressBar заполнялся корректно, необходимо отследить момент, когда на сервер отправляется команда REST и выделить из строки команды значение позиции в файле.

Строка на выполнение команды REST может выглядеть, например, так:

REST 12567899

Соответственно, нам необходимо выделить из этой строки число 12567899 и указать его в свойстве ProgressBar.Position. Делать это удобно в обработчике события OnStatus для свойства FTPSend.Sock. Этот обработчик у нас уже есть в программе и нам его, опять же, достаточно дописать следующим образом:

procedure Tfmain.Status(Sender: TObject; Response: Boolean; const Value: string);
begin
if Response then
  memLog.Lines.Add('--&gt; '+Value)
else
  memLog.Lines.Add('&lt;-- '+Value); //на сервер отправляется команда REST if pos('REST', Value)&gt;0 then
  ProgressBar1.Position:=StrToInt64Def(copy(value,Pos('REST',Value)+5,Length(Value)-Pos('REST',Value)-4),0)
end;

Теперь с реализацией докачки файлов все в порядке – если сервер поддерживает докачку, то после разрыва связи файл именно докачивает, а ProgressBar корректно реагирует на восстановление процесса скачивания/загрузки файлов.

Четвертый пример: выполнение произвольных команд

Бывают случаи, когда имеющихся методов и свойств класса Synapse оказывается недостаточно для выполнения реализации каких-либо функций в программе. Например, у класса TFTPSend отсутствует метод с помощью которого можно изменять атрибуты файлов на сервере. Для реализации такой возможности нам необходимо использовать метод FTPCommand и самостоятельно разбирать ответ сервера. Ниже рассмотрены два примера использования этой полезной функции в Synapse.

Копируем исходный код предыдущего примера в отдельную папку и переименовываем проект, например, в syna_ftp_commands.

Теперь на главную форму приложения добавляем компоненты, как показано на рисунке ниже:

ftp_5

  • CommandBox: TComboBox – выпадающий список в который мы будем записывать все поддерживаемые команды
  • edCmdParams: TEdit – текстовое поле в которое мы впоследствии будем записывать параметры команды
  • btnCmd: TButton – кнопка «Выполнить» клик по которой будет выполнять произвольную команду

Теперь нам необходимо научиться получать список команд, поддерживаемых FTP-сервером. Для получения списка поддерживаемых команд достаточно отправить на сервер команду HELP без каких-либо дополнительных параметров. В результате FTP-сервер вернет в ответе следующий ответ (список команд в ответе может отличаться в зависимости от настроек сервера):
214-The following commands are recognized.
ABOR ACCT ALLO APPE CDUP CWD  DELE EPRT EPSV FEAT HELP LIST MDTM MKD
MODE NLST NOOP OPTS PASS PASV PORT PWD  QUIT REIN REST RETR RMD RNFR
RNTO SITE SIZE SMNT STAT STOR STOU STRU SYST TYPE USER XCUP XCWD XMKD
XPWD XRMD
214 Help OK.

Напишем отдельный метод, который будет отправлять на сервер команду HELP и заполнять список CommandBox. Назовем этот метод FTPHelp. Код методы будет следующий:

procedure Tfmain.FTPHelp;
begin
//если команда HELP успешно выполнена
if FTPSend.FTPCommand('HELP')=214 then
  begin
   //удаляем из полного текста ответа сервера первую и последнюю строку
    FTPSend.FullResult.Delete(0);
    FTPSend.FullResult.Delete(FTPSend.FullResult.Count-1);
    //очищаем ComboBox и записываем в него список доступных команд
    CommandBox.Clear;
    CommandBox.Items.DelimitedText:=FTPSend.FullResult.Text;
  end;
end;

Здесь мы, в случае положительного ответа сервера, удаляем из FullResult (полного ответа сервера) первую и последнюю строки, т.к. в них не содержаться названия команд, а затем оставшуюся строку передаем в CommandBox.

Вызывать этот метод мы будем сразу после того, как происходит авторизация на сервере, т.е. необходимо в обработчике OnClick кнопки btnLogin добавить следующую строку:

procedure Tfmain.btnLoginClick(Sender: TObject);
...
begin
if btnLogin.Caption=cLogin then
begin
....
if FTPSend.Login then
begin
...
//считываем список доступных команд
FTPHelp;
end
...
end;

Проверим работу программы. Запускаем приложение, авторизуемся на сервере и получаем следующий результат:

ftp_6

Теперь переходим к следующей части примера – попробуем выполнить какую-либо произвольную команду. Как было сказано выше, у TFTPSend нет метода с помощью которого можно было бы изменять атрибуты файла. Разберемся как можно выполнить это действие на примере ниже.

За выполнение специфичных для сервера команд, как раз таких как смена атрибутов файла или директории, отвечает команда SITE. Так, для того, чтобы изменить атрибуты файла с именем readme.html на значение 777 нам необходимо отправить на сервер  команду SITE со следующими аргументами:
SITE CHMOD 777 readme.html
и, в случае, если команды будет выполнена успешно, сервер вернет следующую строку ответа:
200 SITE CHMOD command ok.

Напишем обработчик OnClick кнопки «Выполнить» следующим образом:

procedure Tfmain.btnCmdClick(Sender: TObject);
begin
//если не выбрана команда - выходим
if CommandBox.ItemIndex0 then
  FTPSend.FTPCommand(CommandBox.Text+' '+edCmdParams.Text)
else
  FTPSend.FTPCommand(CommandBox.Text);
//выводим ответ сервера пользователю
ShowMessage(FTPSend.FullResult.Text);
end;

Теперь можно запустить приложение и попробовать выполнить, например, команду SITE. Результат представлен на рисунке:

ftp_7

Аналогичным образом можно выполнять и другие FTP-команды. При этом следует отметить, что для получения результата выполнения команды, наравне со свойством TFTPSend FullResult можно также использовать свойства ResultCode и ResultString.

Предыдущая глава книги: «Глава 1 «Работа с HTTP в Synapse»«

Книжная полка

Описание: Рассмотрены практические вопросы по разработке клиент-серверных приложений в среде Delphi 7 и Delphi 2005 с использованием СУБД MS SQL Server 2000, InterBase и Firebird. Приведена информация о теории построения реляционных баз данных и языке SQL. Освещены вопросы эксплуатации и администрирования СУБД.
купить книгу delphi на ЛитРес
Описание: Рассмотрены малоосвещенные вопросы программирования в Delphi. Описаны методы интеграции VCL и API. Показаны внутренние механизмы VCL и приведены примеры вмешательства в эти механизмы. Рассмотрено использование сокетов в Delphi: различные режимы их работы, особенности для протоколов TCP и UDP и др.
купить книгу delphi на ЛитРес
4 1 голос
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
29 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
Estra
Estra
01/04/2014 02:29

Спасибо! Выложили бы еще и первую главу в PDF, цены бы вам небыло!!!

Estra
Estra
02/04/2014 00:16
Ответить на  Vlad

Спасибо!

balmo
balmo
03/04/2014 15:55

Привет, Влад. Мы как-то потерялись. Может Я ненароком в спам-листе оказался?

trackback

[…] глава книги «Глава 2 «Работа с FTP в Synapse»« Предыдущая глава книги «Глава 0 […]

Наталья
Наталья
27/06/2014 01:11

Влад, нужна ваша помощь! Можете с базой данных помочь? несколько частных уроков… Пожаааалуйста. Моя почта nhramcova@yandex.ru

Proger
Proger
17/07/2014 22:37

Ребята, в Synapse с httpsend какой-то косяк! httpsend:=THTTPSend.Create; HTTPSend.Protocol:=’1.1′; HTTPSend.TargetPort:=’80’; HTTPSend.TargetHost:=’143.123.45.12′; //левый IP, который нам никогда не ответит httpsend.UserAgent:=’Opera/12.0(Windows NT 5.1;U;ru)Presto/22.9.168 Version/12.00′; httpsend.Headers.Add(‘Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8’); httpsend.Headers.Add(‘Accept-Language: ru,en-us;q=0.7,en;q=0.3’); httpsend.Headers.Add(‘Accept-Encoding: gzip,deflate’); httpsend.Headers.Add(‘Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.7’); HTTPSend.KeepAliveTimeout:=300; httpsend.KeepAlive:=true; //Любое из этого httpsend.Timeout:=10000; httpsend.Sock.SocksTimeout:=10000; httpsend.Sock.SetTimeout(10000); httpsend.Sock.SetSendTimeout(10000); httpsend.Sock.SetRecvTimeout(10000); Memo1.Lines.Add(‘START: [‘+FormatDateTime(‘c’,Now)+’]’); stream:=TStringStream.Create(»); if HTTPSend.HTTPMethod(‘GET’,’143.123.45.12′) then begin end; Memo1.Lines.Add(‘STOP : [‘+FormatDateTime(‘c’,Now)+’]’); stream.Free; httpsend.Free; exit; Имеем всегда магические 21 секунды ожидания. Не 10, не 20, не 1000, а именно 21! И не важно что мы пишем в таймауте. Слабое место synapse — таймауты. Как же изменить таймаут ожидания? Вообще сперва мне нужно было его уменьшить, а сейчас уже нужно увеличить, но… Подробнее »

Proger
Proger
18/07/2014 18:10

Ребята, кто поможет по проблеме, которую я выше описал, без вопросов 1000 вмр переведу

Proger
Proger
18/07/2014 20:43
Ответить на  Vlad

Всё равно, от своих слов не отказываюсь.
И если раньше найду решение, тоже отпишусь сюда.

Proger
Proger
23/07/2014 16:14
Ответить на  Vlad

HTTPSend.Sock.ConnectionTimeout:=10000;
вызывает у меня ошибку: Undeclared identifier: ‘ConnectionTimeout’

У вас какая версия синапс?
Project : Ararat Synapse | 003.012.006

Proger
Proger
23/07/2014 16:25
Ответить на  Vlad

Очень странно. Сделал поиск по всем «.pas» в составе Синапс версия 40 скаченного с офф сайта http://www.ararat.cz/synapse/doku.php/download по слову «ConnectionTimeout» — ничего не найдено.

Proger
Proger
23/07/2014 16:36
Ответить на  Vlad

Зашел blcksock.pas, который шел в 40 релизе, там версия стоит: 009.008.005
У вас 009.009.001. В репозитарии тоже стоит версия 009.009.001. Во дела!
Сейчас скачаю оттуда и проверю.

Proger
Proger
23/07/2014 17:16
Ответить на  Vlad

Обновил все классы в составе синапс. 22 осталось без изменений (40-ая версия, последняя), а вот другие поменялись. И действительно! Таймаут заработал :) а я через «жэ» уже сделал в свое время проверку работосопособности прокси (а то 21 секунду ждать не вариант было)

Можешь на мыло прислать кошель вмр, скину обещенное

Proger
Proger
23/07/2014 17:32
Ответить на  Vlad

Скинул так.
‘»Скажи «Спасибо»!’ — ее не заметно вообще. При большом объеме статьи к кол-ве комментариев она пропадает где-то в середине

Proger
Proger
23/07/2014 18:35
Ответить на  Vlad


httpsend.Timeout:=4000;
HTTPSend.Sock.ConnectionTimeout:=3000;

httpsend.Sock.SocksTimeout:=6000;
httpsend.Sock.SetTimeout(6000);
httpsend.Sock.SetSendTimeout(6000);
httpsend.Sock.SetRecvTimeout(6000);

Решил использовать полученный бонус. Беру крупный файл, скачиваю его. 6 секунд на скачивание, иначе сброс. Но скачивание идет до конца, процесс после 6 сек не обрывается. Вроде верно всё выставил. Какие еще таймауты можно выставить?

Proger
Proger
23/07/2014 19:11
Ответить на  Vlad

Аха, уже нашел в исходнике
procedure TBlockSocket.SetTimeout(Timeout: Integer);
begin
SetSendTimeout(Timeout);
SetRecvTimeout(Timeout);
end;
httpsend.Sock.InterPacketTimeout:=false; не повлиял на картину.

Den
Den
17/01/2015 17:26

Не получается реализовать докачку с ftp я так понимаю это вообще не работает
Выложите реализованный исходник с работающей докачкой