Электронная библиотека книг Александра Фролова и Григория Фролова.
Shop2You.ru Создайте свой интернет-магазин
Библиотека
Братьев
Фроловых

Разработка приложений для Internet

© Александр Фролов, Григорий Фролов
Том 31, М.: Диалог-МИФИ, 1997, 286 стр.

[Назад] [Содеожание] [Дальше]

Загрузка файлов с сервера FTP

Приложение FtpView, рассмотренное нами в предыдущем разделе, позволяет только просматривать структуру каталогов сервера FTP. С его помощью вы не сможете загрузить файл с сервера на диск локального компьютера или наоборот, записать файл на сервер FTP. В этом разделе мы расширим возможности приложения FtpView, так чтобы его можно было использовать для загрузки файлов с сервера.

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

Процедура загрузки файла с сервера FTP

Чтобы добавить к классу CFtpViewDlg метод FtpFileDownload, воспользуйтесь средствами MFC ClassWizard. В определении класса CFtpViewDlg появится объявление нового метода (листинг 2.10). Соответствующий фрагмент кода мы привели в листинге 3.3. Определение метода FtpFileDownload будет добавлено в файле FtpViewDlg.cpp. Исправьте этот метод так, как это показано в листинге 2.11.

Листинг 2.10. Файл FtpViewDlg.h, фрагмент определения класса CFtpViewDlg


//////////////////////////////////////////////////////////////
// Класс CFtpViewDlg 

class CFtpViewDlg : public CDialog
{
. . .
   DECLARE_MESSAGE_MAP()
private:
   BOOL FtpFileDownload( CString sFileName );
};

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

Листинг 2.11. Файл FtpViewDlg.cpp, метод FtpFileDownload


//============================================================
// Метод FtpFileDownload класса CFtpViewDlg 
// Загружает файл с сервера FTP 
//============================================================
BOOL CFtpViewDlg::FtpFileDownload( CString sFileName )
{
   BOOL fResult;
   CString sLocalFileName;

   // Определяем объект класса CFileDialog, представляющий 
   // стандартную диалоговую панель Save As, в которой
   // по умолчанию выбрано имя файла sFileName
   CFileDialog mFileOpen(FALSE, NULL, sFileName);

   // Отображаем диалоговую панель Open и позволяем 
   // пользователю выбрать с помощью нее один файл
   int result = mFileOpen.DoModal();

   // Проверяем как была закрыта диалоговая панель Open - 
   // по нажатию кнопки OK или Cancel
   if(result == IDCANCEL) 
   {
      // Если пользователь отказался от выбора файла и 
      // нажал кнопку Cancel отображаем соответствующее 
      // сообщение и возвращаем значение FALSE
      AfxMessageBox("File not selected");

      return FALSE;
   }

   else if(result == IDOK)
   {
      // Если пользователь нажал кнопку OK, определяем 
      // имя выбранного файла
      sLocalFileName = mFileOpen.GetPathName();
   }

   sFileName = sCurentDirectory + "/" + sFileName;

   fResult = 
     m_FtpConnection -> 
        GetFile(sFileName.GetBuffer(MIN_LEN_BUF),
                sLocalFileName.GetBuffer(MIN_LEN_BUF),
                FALSE
        );
   return fResult;
}

Остается только исправить метод OnDblclkFtpList так, чтобы когда пользователь выберет файл из списка в главной диалоговой панели приложения, то выполнялся вызов метода FtpFileDownload класса CFtpViewDlg. На листинге 2.12 мы привели фрагмент файла FtpViewDlg.cpp, который содержит новый вариант метода OnDblclkFtpList.

Листинг 2.12. Файл FtpViewDlg.cpp, метод OnDblclkFtpList

//============================================================ // Метод OnDblclkFtpList класса CFtpViewDlg // Переходит в выбранный каталог //============================================================ void CFtpViewDlg::OnDblclkFtpList(NMHDR* pNMHDR, LRESULT* pResult) { int iTotalNumber; // Количество элементов списка CString sSelItem; // Название каталога CString sLength_Dir; // Длина файла или строка Dir // Блокируем список IDC_FTP_LIST m_FtpList.EnableWindow(FALSE); // Определяем количество элементов в списке IDC_FTP_LIST iTotalNumber = m_FtpList.GetItemCount(); // Определяем, какой объект списка выбран for(int i = 0; i < iTotalNumber; i++) { // Атрибут LVIS_SELECTED установлен // у выбранного элемента списка if(LVIS_SELECTED == m_FtpList.GetItemState( i, LVIS_SELECTED )) { // Определяем название выбранного элемента списка // (имя файла или каталога) sSelItem = m_FtpList.GetItemText( i, 0 ); // Считываем данные из колонки Length sLength_Dir = m_FtpList.GetItemText( i, 1 ); if(sLength_Dir == "Dir") // Выбран каталог { // Переходим в выбранный каталог sCurentDirectory = sCurentDirectory + "/" + sSelItem; // Меняем форму курсора CWaitCursor wait; // Отображаем содержимое выбранного каталога DirectoryView(); // Отображаем новый путь каталога m_Status.SetWindowText(sCurentDirectory); } else // Выбран файл { // Меняем форму курсора CWaitCursor wait; FtpFileDownload(sSelItem); } break; } } // Снимаем блокировку списка IDC_FTP_LIST m_FtpList.EnableWindow(TRUE); *pResult = 0; }

Заново постройте проект и запустите приложение. На экране появится главная диалоговая панель, с помощью которой вы можете соединиться с сервером FTP и просмотреть содержимое его каталогов.

Однако теперь, когда вы делаете двойной щелчок левой клавишей мыши по имени файла, вместо предупреждающего сообщения на экране появляется стандартная диалоговая панель Save As с предложением сохранить выбранный файл на локальном диске компьютера.

На рисунке 2.14 мы показали предложение приложения FtpView сохранить файл с именем readme.txt, расположенный на сервере FTP корпорации Microsoft (адрес ftp.microsoft.com) в каталоге developer, в файле readme.txt на локальном диске компьютера в каталоге Library.

Рис. 2.14. Выбор имени файла на локальном компьютере

По умолчанию в панели Save As вам предлагается имя файла, соответствующее имени исходного файла на сервере FTP. Вы можете оставить его без изменения или поменять по своему усмотрению. Если вы выберите на локальном компьютере уже существующий файл, когда приложение запросит подтверждение на его замену (рис. 2.15).

Рис. 2.15. Файл уже существует

Рассмотрим как работают методы OnDblclkFtpList и FtpFileDownload.

Метод OnDblclkFtpList

Когда из списка файлов и каталогов сервера FTP вы выбираете файл и делаете по его имени двойной щелчок левой клавишей мыши, вызывается метод OnDblclkFtpList.

Он блокирует список IDC_FTP_LIST и определяет сколько элементов в нем содержится. Далее в цикле находится индекс выбранного элемента списка и считываются его название (колонка Name) и длина (колонка Length). До этого момента программный код метода OnDblclkFtpList совпадает с первым вариантом метода приложения FtpView. Далее начинаются отличия:


// Определяем название выбранного элемента списка
// (имя файла или каталога)
sSelItem = m_FtpList.GetItemText( i, 0 );

// Считываем данные из колонки Length
sLength_Dir = m_FtpList.GetItemText( i, 1 );

Если в колонке Length записана строка Dir, значит данный элемент описывает каталог. В этом случае мы переходим в выбранный каталог, для чего добавляем к строке sCurentDirectory имя выбранного каталога.

Затем изменяем форму курсора и вызываем метод DirectoryView. Он обновляет информацию в списке, заполняя ее именами каталогов и файлов из каталога sCurentDirectory сервера FTP:


if(sLength_Dir == "Dir") // Выбран каталог
{
   // Переходим в выбранный каталог
   sCurentDirectory  = 
   sCurentDirectory + "/" + sSelItem;

   // Меняем форму курсора
   CWaitCursor wait;   

   // Отображаем содержимое выбранного каталога 
   DirectoryView();

   // Отображаем новый путь каталога
   m_Status.SetWindowText(sCurentDirectory);
}

Если же из списка выбран не каталог, а файл, то порядок действий другой. В этом случае мы также сначала изменяем форму курсора, а затем вызываем метод FtpFileDownload класса CFtpViewDlg, передавая ему в качестве параметра имя выбранного файла:

else // Выбран файл { // Меняем форму курсора CWaitCursor wait; FtpFileDownload(sSelItem); }

Таким образом, всю основную работу выполняет метод FtpFileDownload, который мы рассмотрим ниже.

Метод FtpFileDownload

Метод FtpFileDownload имеет единственный параметр, через который передается имя файла, выбранного пользователем из списка объектов сервера FTP:


BOOL CFtpViewDlg::FtpFileDownload( CString sFileName )
{
   . . .
}

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

Для облегчения этой задачи мы воспользовались классом CFileDialog, который входит в состав библиотеки классов MFC и предназначен для управления стандартной диалоговой панелью Save As. Объявляем объект mFileOpen этого класса:


CFileDialog mFileOpen(FALSE, NULL, sFileName);

¨     Класс CFileDialog, а также некоторые другие классы стандартных диалоговых панелей были описаны нами в 28 томе серии “Библиотека системного программиста”.

Конструктору класса CFileDialog мы указали три параметра. Первый параметр имеет значение FALSE и определяет, что объект mFileOpen будет управлять диалоговой панелью Save As. Если бы мы указали вместо FALSE значение TRUE, то была бы создана диалоговая панель Open.

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

Третий параметр конструктора sFileName содержит имя файла, которое мы выбрали из списка. Это имя будет предложено в диалоговой панели Save As по умолчанию.

Конструктор класса CFileDialog не открывает диалоговой панели Save As. Он только создает управляющий объект mFileOpen. Чтобы диалоговая панель Save As появилась на экране вызывается метод DoModal класса CFileDialog:


result = mFileOpen.DoModal();

Метод DoModal создает модальную диалоговую панель. Поэтому он возвращает управление только тогда, когда пользователь выберет файл или откажется от выбора и нажмет кнопку Cancel.

Если пользователь откажется от выбора и нажмет кнопку Cancel, то метод DoModal вернет значение IDCANCEL. Тогда мы выводим на экран соответствующее сообщение и завершаем метод FtpFileDownload, возвращая значение FALSE:


// Проверяем как была закрыта диалоговая панель Open - 
// по нажатию кнопки OK или Cancel
if(result == IDCANCEL) 
{
   // Если пользователь отказался от выбора файлов и 
   // нажал кнопку Cancel отображаем соответствующее 
   // сообщение и возвращаем значение FALSE
   AfxMessageBox("File not selected");

   return FALSE;
}

Если пользователь выбрал файл из диалоговой панели Save As, то метод DoModal возвращает значение IDOK. В этом случае мы определяем полный путь файла, включая имя диска, путь каталога и имя файла и записываем его во временную строку sLocalFileName. Эта строка определена в данном метода как объект класса CString:


else if(result == IDOK)
{
   sLocalFileName = mFileOpen.GetPathName();
}

Через параметр sFileName методу FtpFileDownload было передано имя файла, выбранного пользователем из списка файлов и каталогов сервера FTP. Мы не стали передавать полный путь файла целиком, так как имя файла можно использовать для инициализации диалоговой панели Save As. Текущий путь каталога сервера FTP записан в строке sCurentDirectory, поэтому чтобы сформировать полный путь файла, выбранного на сервере, достаточно выполнить следующую операцию:


sFileName = sCurentDirectory + "/" + sFileName;

Теперь в строке sFileName записан полный путь файла, расположенного на сервере FTP, а в строке sLocalFileName - полный путь файла на диске локального компьютера.

Остается загрузить файл с сервера FTP. Для этого мы вызываем метод GetFile, входящий в класс CFtpConnection. Метод GetFile имеет много различных параметров, но в нашем приложении мы используем только три. Первый параметр задает полный путь файла, который надо получить с сервера FTP, второй - полный путь файла на локальном компьютере в который полученный файл будет записан, а третий - определяет поведение метода в том случае если второй параметр задает файл, уже существующий на диске. В данном случае в качестве третьего параметра используется значение FALSE. Оно означает, что если на диске локального компьютера уже есть файл с таким именем, он перезаписывается (предварительно у пользователя запрашивается подтверждение на перезапись файла). Вы можете использовать в качестве третьего параметра значение TRUE или вовсе его не указывать. Тогда метод GetFile не будет перезаписывать уже существующий файл, а просто завершится с ошибкой:


fResult = m_FtpConnection -> 
   GetFile( sFileName.GetBuffer(MIN_LEN_BUF),
            sLocalFileName.GetBuffer(MIN_LEN_BUF),
            FALSE
          );

В случае успешного завершения метод GetFile возвращает ненулевое значение, а если произошла какая-нибудь ошибка - нуль. Мы используем значение, возвращаемое методом GetFile в качестве результата работы самого метода FtpFileDownload:


return fResult;

Недостатки приложения FtpView

У приложения FtpView, рассмотренном в предыдущем разделе, есть как минимум один большой недостаток. В то время когда идет загрузка файла с сервера FTP, ход этого процесса никак не отображается и приложение создает впечатление зависшей программы. Только те, кто использует внешний модем, могут догадаться о работе приложения по интенсивному или не очень интенсивному миганию индикаторов на его лицевой панели. Если компьютер оборудован внутренним модемом, вы в принципе также можете следить за его состоянием, если разместите в панели управления Windows 95 (или Windows NT версии 4.0) индикатор работы модема.

Этот недостаток нашего приложения обусловлен устройством метода GetFile класса CFtpConnection, так как он не предусматривает возможности извещения приложения о загрузке части файла. Поэтому вместо использования метода GetFile мы предлагаем воспользоваться методом OpenFile класса CFtpConnection и методом Read класса CInternetFile. Метод OpenFile открывает файл, расположенный на сервере, а метод Read позволяет считывать открытый файл по частям.

Кроме того, метод GetFile сразу записывает файл, полученный с сервера, в файл на локальном диске компьютера. Метод Read записывает полученный фрагмент файла с сервера во временный буфер. Это позволяет, например, обеспечить дополнительную обработку данных по мере их получения.

Доработаем приложение FtpView так, чтобы оно показывало ход загрузки файлов с сервера FTP. Для этого сначала загрузите в редактор ресурсов диалоговую панель IDD_FTPVIEW_DIALOG. Немного увеличьте вертикальный размер панели и добавьте внизу панели линейный индикатор progress bar, выбрав для него идентификатор IDC_PROGRESS, и текстовую строку Progress (рис. 2.13). Фрагмент файла FtpView.rc, содержащий шаблон диалоговой панели мы привели в листинге 4.4.

Листинг 2.13. Фрагмент файла FtpView.rc


//////////////////////////////////////////////////////////////
//
// Dialog
//

IDD_FTPVIEW_DIALOG DIALOGEX 0, 0, 352, 215
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | 
                      WS_SYSMENU
EXSTYLE WS_EX_APPWINDOW
CAPTION "FtpView"
FONT 8, "MS Sans Serif"
BEGIN
  PUSHBUTTON      "Connect",IDC_CONNECT,196,5,45,15
  PUSHBUTTON      "On top",IDC_ON_TOP,253,5,40,15
  DEFPUSHBUTTON   "OK",IDOK,305,5,40,15

  CONTROL    "List1",IDC_FTP_LIST,"SysListView32",LVS_REPORT | 
              LVS_SORTASCENDING | LVS_ALIGNLEFT | WS_BORDER | 
              WS_TABSTOP,5,25,340,133

  EDITTEXT    IDC_FTP_ADDRESS,5,5,183,14,ES_AUTOHSCROLL
  LTEXT       "Directory:",IDC_STATIC,7,171,33,8
  EDITTEXT    IDC_STATUS,44,169,301,15,ES_AUTOHSCROLL | 
              ES_READONLY

  CONTROL     "Progress1",IDC_PROGRESS, "msctls_progress32", 
              WS_BORDER, 43,193,302,14

  LTEXT       "Progress:",IDC_STATIC,7,196,30,8
END

Запустите MFC ClassWizard, перейдите на страницу Member Variables, выберите класс CFtpViewDlg и добавьте к нему управляющий объект для линейного индикатора. Для этого выберите из списка Control IDs идентификатор IDC_PROGRESS и нажмите кнопку Add Variable. Вам будет предложено привязать к линейному индикатору объект класса CProgressCtrl. Введите имя этого объекта. В нашем случае мы использовали имя m_LoadProgress. Нажмите кнопку OK и закройте MFC ClassWizard.

В классе CFtpViewDlg появится новый элемент m_LoadProgress. Он будет добавлен в блоке AFX_DATA, которым управляет MFC ClassWizard:


class CFtpViewDlg : public CDialog
{
// Construction
public:
   ~CFtpViewDlg();
   CFtpViewDlg(CWnd* pParent = NULL); 

// Dialog Data
   //{{AFX_DATA(CFtpViewDlg)
   enum { IDD = IDD_FTPVIEW_DIALOG };
   CProgressCtrl   m_LoadProgress;
   . . .
   //}}AFX_DATA
   . . .
}

Кроме того, вносятся изменения в метод DoDataExchange класса CFtpViewDlg. В блоке AFX_DATA_MAP, в котором MFC ClassWizard размещает методы для обмена данными, добавляется новая строка. Она устанавливает связь между линейным индикатором IDC_PROGRESS и объектом m_LoadProgress:


DDX_Control(pDX, IDC_PROGRESS, m_LoadProgress);

Перед тем как использовать линейный индикатор, следует выполнить его настройку - установить границы значений в которых он может меняться и шаг приращения. В принципе, вы можете оставить эти значения по умолчанию. В этом случае линейный индикатор будет отображать значения в интервале от 0 до 100 с шагом 10. Однако мы все же выполним инициализацию линейного индикатора, чтобы программный код приложения был более нагляден.

Загрузите в текстовый редактор Microsoft Visual C++ метод OnInitDialog класса CFtpViewDlg и добавьте к нему вызовы методов SetRange и SetStep, как это показано на листинге 2.14. Обратите внимание, что эти методы вызываются для объекта m_LoadProgress, представляющего линейный индикатор IDC_PROGRESS.

Листинг 2.14. Фрагмент файла FtpViewDlg.cpp, метод OnInitDialog класса CFtpViewDlg


BOOL CFtpViewDlg::OnInitDialog()
{
   // Вызываем метод OnInitDialog базового класса CDialog
   CDialog::OnInitDialog();

   // Устанавливаем пиктограммы, которые будут отображаться 
   // в случае минимизации диалоговой панели 
   SetIcon(m_hIcon,TRUE);  // Пиктограмма стандартного размера
   SetIcon(m_hIcon,FALSE); // Пиктограмма маленького размера

   //=========================================================
   // Устанавливаем границы для линейного индикатора
   m_LoadProgress.SetRange(0, 100);

   // Устанавливаем шаг приращения для линейного индикатора
   m_LoadProgress.SetStep(10);

   //=========================================================
   // Выполняем инициализацию списка IDC_FTP_LIST
   //=========================================================
   . . .
}

Наибольшие изменения надо внести в метод FtpFileDownload класса CFtpViewDlg. Мы должны изменить способ загрузки файла с сервера - отказаться от использования метода GetFile и принимать файл по частям, используя методы OpenFile и Read. Так как в этом случае принимаемые данные не записываются в файл, а помещаются во временный буфер, необходимо организовать запись данных из буфера в файл на локальном диске. Для этого мы будем использовать возможности класса CFile. И наконец, по мере загрузки файла с сервера мы будем изменять состояние линейного индикатора IDC_PROGRESS.

Модифицированный метод FtpFileDownload класса CFtpViewDlg, выполняющий все описанные выше действия представлен в листинге 2.15.

Листинг 2.15. Фрагмент файла FtpViewDlg.cpp, метод FtpFileDownload класса CFtpViewDlg


BOOL CFtpViewDlg::FtpFileDownload( CString sFileName )
{
   BOOL fResult;
   CString sLocalFileName;

   // Определяем объект класса CFileDialog, представляющий 
   // стандартную диалоговую панель Save As, в которой
   // по умолчанию выбрано имя файла sFileName
   CFileDialog mFileOpen(FALSE, NULL, sFileName);

   // Отображаем диалоговую панель Open и позволяем 
   // пользователю выбрать с помощью нее один файл
   int result = mFileOpen.DoModal();

   // Проверяем как была закрыта диалоговая панель Open - 
   // по нажатию кнопки OK или Cancel
   if(result == IDCANCEL) 
   {
      // Если пользователь отказался от выбора файлов и 
      // нажал кнопку Cancel отображаем соответствующее 
      // сообщение и возвращаем значение FALSE
      AfxMessageBox("File not selected");

      return FALSE;
   }

   else if(result == IDOK)
   {
      // Если пользователь нажал кнопку OK, определяем 
      // имя выбранного файла
      sLocalFileName = mFileOpen.GetPathName();
   }

   //=========================================================
   // Формируем полное имя файла для загрузки его с 
   // сервера FTP
   sFileName = sCurentDirectory + "/" + sFileName;

   //=========================================================
   // Чтобы узнать размер файла, записанного на сервере FTP, 
   // создаем объект класса CFtpFileFind
   CFtpFileFind m_FtpFileFind(m_FtpConnection);

   // Выполняем поиск выбранного нами файла
   if(fResult = 
      m_FtpFileFind.FindFile(_T(sFileName)))
   {
      // Если поиск закончился успешно, получаем его 
      // характеристики
      fResult = m_FtpFileFind.FindNextFile();

      // Временная переменная для хранения размера файла 
      DWORD dwLength = 0;

      // Определяем длину файла
      dwLength = m_FtpFileFind.GetLength();

      // В соответствии с размером файла устанавливаем новые 
      // границы для линейного индикатора 
      m_LoadProgress.SetRange(0, (int)(dwLength/READ_BLOCK) );

      // Устанавливаем шаг приращения для линейного индикатора
      m_LoadProgress.SetStep(1);
   }

   // Так как мы искали только один файл, заканчиваем поиск
   m_FtpFileFind.Close();

   // Определяем указатель на файл сервера 
   CInternetFile* iFile;

   // Открываем выбранный нами файл на сервере FTP 
   iFile = m_FtpConnection -> OpenFile( 
      sFileName.GetBuffer(MIN_LEN_BUF), 
      GENERIC_READ, 
      FTP_TRANSFER_TYPE_BINARY
   );

   //=========================================================
   // Создаем и открываем файл на локальном диске компьютера
   CFile fLocalFile( 
            sLocalFileName, 
            CFile::modeCreate | CFile::modeWrite 
         );

   //=========================================================
   // Временная переменная. В нее заносится количество байт, 
   // считанное с файла на сервере
   UINT nReadCount = 0;
   
   // Создаем буфер для чтения файла с сервера FTP 
   void* ptrBuffer;
   ptrBuffer = malloc( READ_BLOCK );

   // Считываем файл с сервера, записываем его в локальный 
   // файл и изменяем текущее положение линейного индикатора
   do
   {
      // Если во время операции чтения возникнет исключение 
      // CInternetException, то оно будет обработано 
      // соответствующим блоком catch
      try
      {
         // Читаем из файла на сервере READ_BLOCK байт
         nReadCount = iFile -> Read( ptrBuffer, READ_BLOCK );
      }
      catch (CInternetException* pEx)
      {
         // Обрабатываем исключение CInternetException
         TCHAR szErr[1024];  // Временный буфер для сообщения

         // Выводим сообщение об ошибке
         if (pEx->GetErrorMessage(szErr, 1024))
            AfxMessageBox(szErr);
         else
            AfxMessageBox("GetFtpConnection Error");

         // Так как возникла ошибка, значит данные не считаны
         nReadCount = 0;
         // Удаляем исключение
         pEx->Delete();
      }

      // Записываем информацию, считанную из файла на сервере,
      // которая находится в буфере в локальный файл 
      fLocalFile.Write( ptrBuffer, nReadCount );

      // Увеличиваем значение на линейном индикаторе
      m_LoadProgress.StepIt();

   // Продолжаем чтение файла с сервера до тех пор, пока не 
   // будет достигнут конец файла
   } while (nReadCount == READ_BLOCK); 

   // После окончания загрузки файла сбрасываем 
   // линейный индикатор
   m_LoadProgress.SetPos( 0 );

   // Закрываем файл на сервере
   iFile -> Close();
   // Удаляем объект iFile
   delete iFile;

   // Закрываем файл на локальном компьютере
   fLocalFile.Close();

   // Освобождаем буфер ptrBuffer
   free( ptrBuffer );

   return fResult;
}

Постройте проект и запустите полученное приложение. Введите адрес сервера FTP и нажмите кнопку Connect. Далее выберите из списка файлов на сервере, файл для загрузке и сделайте по нему двойной щелчок левой кнопкой мыши. Как и ранее, вам будет предложено указать имя файла на локальном компьютере, в который будет записан файл с сервера FTP. Затем начнется процесс загрузки файла, ход которого будет отображаться линейным индикатором в нижней части диалоговой панели приложения (рис. 2.16).

Рис. 2.16. Загрузка файла с сервера FTP

Как вы уже могли заметить, основные изменения в исходных текстах приложения FtpView коснулись только метода FtpFileDownload класса CFtpViewDlg, который собственно и осуществляет загрузку файла с сервера FTP. Рассмотрим подробнее как работает этот метод.

Метод FtpFileDownload класса CFtpViewDlg

Метод FtpFileDownload класса CFtpViewDlg имеет единственный параметр sFileName, через который ему передается строка, содержащая имя файла на сервере FTP. Этот файл надо загрузить на локальный компьютер:


BOOL CFtpViewDlg::FtpFileDownload( CString sFileName )
{
}

Первая часть метода FtpFileDownload практически полностью соответствует более ранним версиям этого метода. В ней создается и отображается на экране стандартная диалоговая панель Save As. Эта панель позволяет пользователю указать имя файла и каталог на локальном компьютере в который записывается файл, загружаемый с сервера FTP. По умолчанию в качестве имени файла предлагается использовать имя загружаемого файла. Это имя передается конструктору класса CFileDialog через параметр sFileName:


// Создаем объект, управляющий панелью Save As
CFileDialog mFileOpen(FALSE, NULL, sFileName);
// Отображаем панель Save As на экране
int result = mFileOpen.DoModal();

После того как пользователь выберет файл и нажмет кнопку OK, или откажется от выбора и нажмет кнопку Cancel, диалоговая панель Save As закрывается и метод DoModal возвращает соответствующий код завершения.

Если пользователь отказался от выбора файла и нажал кнопку Cancel, то метод DoModal возвращает значение IDCANCEL. В этом случае мы только выводим соответствующее предупреждающее сообщение и завершаем работу метода FtpFileDownload не выполняя загрузки файла:


if(result == IDCANCEL) 
{
   AfxMessageBox("File not selected");
   return FALSE;
}

Если пользователь выбрал файл и нажал кнопку OK (или выбрал файл, выполнив по его имени двойной щелчок левой кнопкой мыши), то метод DoModal возвращает значение IDOK. В этом случае мы определяем полный путь выбранного пользователем файла и записываем его в строку sLocalFileName:


CString sLocalFileName;
. . .
else if(result == IDOK)
{
   sLocalFileName = mFileOpen.GetPathName();
}

Через единственный параметр метода FtpFileDownload ему передается строка sFileName, содержащая имя файла выбранного пользователем для загрузки с сервера. Чтобы получить полный путь этого файла на сервере мы добавляем имя файла к текущему каталогу. Результат записываем обратно в строку sFileName:


// Формируем полное имя файла для загрузки его с сервера FTP
sFileName = sCurentDirectory + "/" + sFileName;

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

Сначала мы вызываем метод FindFile, передавая ему в качестве параметра полный путь файла, который надо найти на сервере, а затем, в случае успеха, вызываем метод FindNextFile который позволит нам определить характеристики, и в том числе длину, обнаруженного файла.

Надо заметить, что файл sFileName обязательно будет обнаружен, так как мы уже нашли его раньше (см. метод DirectoryView). Конечно мы не учитываем в этом случае возможность разрыва связи, удаление файла администратором и сервера и т. д.:


// Создаем объект класса CFtpFileFind
CFtpFileFind m_FtpFileFind(m_FtpConnection);

// Выполняем поиск выбранного нами файла
if(fResult = 
   m_FtpFileFind.FindFile(_T(sFileName)))
{
   // Если поиск закончился успешно, получаем его 
   // характеристики
   fResult = m_FtpFileFind.FindNextFile();
   . . . 
}

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


// Временная переменная для хранения размера файла 
DWORD dwLength = 0;

// Определяем длину файла
dwLength = m_FtpFileFind.GetLength();

В соответствии с длинной загружаемого файла устанавливаются новые границы изменения значений для линейного индикатора и новый шаг приращения.

Мы будем загружать файл с сервера FTP блоками по READ_BLOCK байт в каждом (последний блок может иметь меньшую длину). Поэтому мы сможем изменить состояние линейного индикатора dwLength / READ_BLOCK раз:


// Устанавливаем новые границы для линейного индикатора
m_LoadProgress.SetRange(0, (int)(dwLength/READ_BLOCK) );

// Устанавливаем шаг приращения равный единице
m_LoadProgress.SetStep(1);

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

Перед тем как приступить к самому интересному - загрузке файла с сервера FTP, заканчиваем поиск и вызываем метод Close для объекта m_FtpFileFind:


// Так как мы искали только один файл, заканчиваем поиск
m_FtpFileFind.Close();

Перед тем как приступить к загрузке файла с сервера мы должны его открыть. Для этого следует воспользоваться методом OpenFile, входящим в состав класса CFtpConnection. Он возвращает указатель на объект класса CInternetFile:


// Определяем указатель на файл Internet  
CInternetFile* iFile;

// Открываем выбранный нами файл на сервере FTP 
iFile = m_FtpConnection -> OpenFile( 
   sFileName.GetBuffer(MIN_LEN_BUF), 
   GENERIC_READ, 
   FTP_TRANSFER_TYPE_BINARY
);

Первый параметр метода OpenFile задает имя файла, который надо открыть. Второй параметр выбирает режим в котором открывается файл. Константа GENERIC_READ означает, что файл открывается для чтения. Третий и последний параметр метода OpenFile устанавливает режим передачи двоичных данных. Мы выбрали этот режим чтобы иметь возможность загружать не только текстовые, но и двоичные файлы.

Файл, получаемый с сервера FTP, мы должны сохранить на локальном компьютере в файле под именем sLocalFileName. Создаем файл sLocalFileName, используя возможности класса CFile:


// Создаем и открываем файл на локальном диске компьютера
CFile fLocalFile( 
         sLocalFileName, 
         CFile::modeCreate | CFile::modeWrite 
      );

Файл создается с атрибутами CFile::modeCreate и CFile::modeWrite, поэтому если файла с именем sLocalFileName нет - он будет создан, а если такой файл уже есть, то он перезаписывается. У пользователя при этом запрашивается подтверждение на перезапись файла.

Для чтения информации из файла на сервере мы будем обращаться к методу Read класса CInternetFile. Этот метод считывает порцию данных из файла и записывает их во временный буфер. Буфер мы предварительно создаем, при помощи функции malloc:


// Создаем буфер для чтения файла с сервера FTP 
void* ptrBuffer;
ptrBuffer = malloc( READ_BLOCK );

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

Операция чтения файла с сервера выполняется методом Read, определенным в классе CInternetFile. Каждый вызов метода Read считывает READ_BLOCK байт и записывает их в буфер ptrBuffer. Количество байт, которое действительно было считано из файла на сервере, возвращается методом Read и записывается во временную переменную nReadCount.

Как только после вызова метода Read значение nReadCount станет меньше READ_BLOCK значит достигнут конец файла и можно выходить из цикла чтения:


UINT nReadCount = 0;

do
{
   . . . 
   // Читаем из файла на сервере READ_BLOCK байт
   nReadCount = iFile -> Read( ptrBuffer, READ_BLOCK );
   . . . 
} while (nReadCount == READ_BLOCK); 

Если во время чтения файла с сервера возникнет ошибка, например разорвется связь, то метод Read вызовет соответствующее исключение CInternetException. Мы организуем обработку такой ситуации и помещаем вызов метода Read в блок try, а ниже определяем блок catch:


try
{
   // Читаем из файла на сервере READ_BLOCK байт
   nReadCount = iFile -> Read( ptrBuffer, READ_BLOCK );
}

Блок catch выполняет обработку исключения CInternetException. Обращаясь к методу GetErrorMessage мы получаем текстовое описание причины, вызвавшей исключение и отображаем ее на экране. Если метод GetErrorMessage не может определить причину исключения, он возвращает нулевое значение и мы отображаем на экране строку GetFtpConnection Error:


catch (CInternetException* pEx)
{
   // Обрабатываем исключение CInternetException
   TCHAR szErr[1024];  // временный буфер для сообщения

   // Выводим сообщение об ошибке
   if (pEx->GetErrorMessage(szErr, 1024))
      AfxMessageBox(szErr);
   else
      AfxMessageBox("GetFtpConnection Error");

   // Так как возникла ошибка, значит данные не считаны
   nReadCount = 0;
   // Удаляем исключение
   pEx->Delete();
}

Чтобы прервать дальнейшее выполнение цикла загрузки файла мы записываем в переменную nReadCount нулевое значение. Затем удаляем исключение, вызывая для него метод Delete.

Данные, которые считаны из файла на сервере и находятся в буфере ptrBuffer, надо записать в файл fLocalFile на локальном диске компьютера. Для этого передаем буфер ptrBuffer методу Write и указываем, что надо записать в файл nReadCount байт. Напомним, что при достижении конца файла на сервере метод Read считывает остаток файла, размер которого может быть меньше размера буфера:


fLocalFile.Write( ptrBuffer, nReadCount );

Когда очередной блок из файла считан, увеличиваем значение линейного индикатора на одну позицию. За время загрузки файла линейный индикатор изменит свое значение от крайне левого до крайне правого:


// Увеличиваем значение на линейном индикаторе
m_LoadProgress.StepIt();

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


// После окончания загрузки файла сбрасываем 
// линейный индикатор
m_LoadProgress.SetPos( 0 );

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


// Закрываем файл на сервере
iFile -> Close();
// Удаляем объект iFile
delete iFile;

Также закрываем и локальный файл, в который скопирован файл с сервера FTP:


// Закрываем файл на локальном компьютере
fLocalFile.Close();

Освобождаем буфер ptrBuffer, вызывая функцию free:


// Освобождаем буфер ptrBuffer
free( ptrBuffer );

Загрузка файла в фоновом режиме

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

В остальном складывается такое впечатление, что приложение “зависло”. Оно даже не перерисовывает свое окно и если временно переключится на другое приложение, окно которого перекроет окно FtpView, то изображение в окне FtpView будет испорчено. Только по окончании загрузки файла наше приложение спохватится и восстановит свое окно.

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

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

За основу мы взяли базовый вариант приложения FtpView, выполняющий загрузку файлов с сервера FTP при помощи метода GetFile. Мы постарались свести все изменения в исходных текстах приложения к минимуму. Вы должны будете только добавить три глобальные переменные, изменить метод FtpFileDownload класса CFtpViewDlg, и добавить функцию ThreadFileLoadProc.

Определение глобальных переменных добавьте в самом начале файла FtpViewDlg.cpp непосредственно после директив препроцессора. Все три новых глобальных переменных global_sFtpAddress, global_sFileName и global_sLocalFileName являются объектами класса CString. Эти переменные используются для передачи задаче, осуществляющей загрузку файла, адреса сервера FTP, пути каталога и имени исходного файла на сервере FTP, а также полного пути файла на диске локального компьютера в который будет записан файл с сервера:


//================== Глобальные переменные ===================
// Адрес сервера FTP
CString global_sFtpAddress = "";    

// Полный путь файла на диске локального компьютера
CString global_sFileName = "";      

// Путь каталога и имя файла на сервере FTP
CString global_sLocalFileName = "";

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

Адрес сервера FTP мы берем из глобальной переменной global_sFtpAddress, путь файла, который надо загрузить с сервера и его название - из переменной global_sFileName, а полный путь файла на диске локального компьютера, куда полученный файл должен быть записан - из переменной global_sLocalFileName:


//============================================================
// Задача, выполняющая загрузку файла с сервера FTP
//============================================================
UINT ThreadFileLoadProc(LPVOID param)
{
   //=========================================================
   // Инициализируем сеанс связи с Internet
   //=========================================================
   CFtpConnection*   m_FtpConnection;   // Сервер FTP 
   CInternetSession* m_InternetSession; // Сеанс связи 

   // Создаем сеанс связи с Internet, указываем в качестве 
   // имени программы-клиента строку FtpProcess
   m_InternetSession = new CInternetSession("FtpProcess");

   // В случае ошибки отображаем сообщение и завершаем 
   // задачу с кодом завершения 1
   if(!m_InternetSession)
   {
      AfxMessageBox("New Session Error", MB_OK);
      return 1;
   }
   
   // Инициализируем указатель m_FtpConnection
   m_FtpConnection = NULL;

   // Пытаемся соединиться с сервером FTP 
   try
   {
      // Соединяемся с сервером FTP. Эта операция
      // может вызвать исключение CInternetException
      m_FtpConnection = m_InternetSession -> 
         GetFtpConnection(global_sFtpAddress);
   }
   catch (CInternetException* pEx)
   {
      // Выводим сообщение об ошибке
      AfxMessageBox("GetFtpConnection Error");

      // Удаляем исключение
      pEx->Delete();

      // Обнуляем указатель m_FtpConnection
      m_FtpConnection = NULL;
   }

   if(m_FtpConnection != NULL)
   {
      BOOL fResult;

      // Загружаем файл с сервера
      fResult = m_FtpConnection -> 
         GetFile(global_sFileName.GetBuffer(MIN_LEN_BUF),
                 global_sLocalFileName.GetBuffer(MIN_LEN_BUF),
                 FALSE
         );

      // Закрываем соединение с сервером
      m_FtpConnection -> Close();
      delete m_FtpConnection;

      // После успешного завершения загрузки выводим сообщение
      if( fResult )
         AfxMessageBox("File load complited:" + 
                        global_sFileName);
   }

   // Завершаем сеанс связи с Internet
   m_InternetSession -> Close();
   delete m_InternetSession;

   return 0;
}

Функция ThreadFileLoadProc выполняет загрузку файла с сервера FTP в файл на диске локального компьютера. Надо только заполнить глобальные переменные global_sFtpAddress, global_sFileName и global_sLocalFileName, а затем запустить функцию ThreadFileLoadProc как отдельную задачу. Для этого измените метод FtpFileDownload класса CFtpViewDlg как это показано ниже:


BOOL CFtpViewDlg::FtpFileDownload( CString sFileName )
{
   BOOL fResult;
   CString sLocalFileName;

   // Определяем объект класса CFileDialog, представляющий 
   // стандартную диалоговую панель Save As, в которой
   // по умолчанию выбрано имя файла sFileName
   CFileDialog mFileOpen(FALSE, NULL, sFileName);

   // Отображаем диалоговую панель Open и позволяем 
   // пользователю выбрать с помощью нее один файл
   int result = mFileOpen.DoModal();

   // Проверяем как была закрыта диалоговая панель Open - 
   // по нажатию кнопки OK или Cancel
   if(result == IDCANCEL) 
   {
      // Если пользователь отказался от выбора файлов и 
      // нажал кнопку Cancel отображаем соответствующее 
      // сообщение и возвращаем значение FALSE
      AfxMessageBox("File not selected");

      return FALSE;
   }

   // Если пользователь нажал кнопку OK 
   else if(result == IDOK)
   {
      // Записываем полный путь файла на диске локального 
      // компьютера
      global_sLocalFileName = mFileOpen.GetPathName();

      // Определяем путь и имя файла, который надо загрузить
      // с сервера FTP 
      global_sFileName = sCurentDirectory + "/" + sFileName;

      // Запоминаем адрес сервера FTP 
      global_sFtpAddress = m_FtpAddress;

      // Запускаем новую задачу, определенную функцией 
      // ThreadFileLoadProc
      AfxBeginThread( ThreadFileLoadProc, NULL, 
                      THREAD_PRIORITY_NORMAL);
   }

   return fResult;
}

Мы не будем подробно описывать функцию ThreadFileLoadProc и новый вариант метода FtpFileDownload класса CFtpViewDlg. В них используются те же приемы, что продемонстрированы в методе FtpFileDownload класса CFtpViewDlg базового варианта приложения FtpView.

[Назад] [Содеожание] [Дальше]