Delphi 6: Réaliser un Client FTP à l'aide des composants Indy



Télécharger les sources du projet (5.89 ko)


Delphi 6 est fourni avec les composants Indy. Voyons comment les utiliser pour mettre en oeuvre un client FTP.

1) Avertissement

Ce tutoriel ayant été réalisé avec la version américaine de Delphi, il est possible qu'il existe des différences entre les textes des éléments utilisés dans cet article et ceux de Delphi 6 version française. Pour cette raison, et pour éviter des traductions approximatives, je donnerais le texte américain...

2) Connaissances préalables

FTP (File Transfert Protocol) est un protocole de transfert de fichier utilisé sur Internet pour envoyer et recevoir des fichiers, mais aussi pour gérer des répertoires distants. Le protocole FTP est généralement utilisé pour la distribution de logiciels, pour l'envoi de fichiers sur un serveur web, ou encore pour mettre à jour votre site web personnel.

Nous allons voir dans cet article comment se connecter à un serveur FTP et manipuler les commandes de base, telles qu'afficher le contenu d'un répertoire distant et télécharger des fichiers. Mais avant de se connecter à un serveur, il faut tout d'abord savoir qu'un serveur demande toujours une authentification, constituée d'un nom d'utilisateur et d'un mot de passe.

Il existe de nombreux serveurs FTP anonymes, où il suffit de mettre "anonymous" comme nom d'utilisateur et de rentrer votre adresse email comme mot de passe. Les serveurs anonymes ne vous permettent généralement pas l'envoi de fichiers, mais uniquement la réception.

Ensuite, la plupart des serveurs FTP utilisent les conventions UNIX et le système ext2fs pour la gestion des fichiers, c'est à dire que le séparateur de répertoires est le slash ( "/" ), qu'il y a une différence entre les minuscules et les majuscules, et que les fichiers ont des droits d'utilisation pour chaque type d'utilisateurs, contrairement à la gestion de fichiers sous Windows9x où tous les fichiers sont accessibles par tous les utilisateurs.

Enfin, il faut savoir que la plupart des serveurs FTP utilisent le Port 21. Un port est en fait une sorte de passerelle pour les données. Les données peuvent être reçues sous forme de texte ou sous forme binaire.

Pour avoir plus de détails sur le protocole FTP, vous trouverez la spécification complète à l'adresse suivante:
http://www.eisti.fr/res/res/rfc959/959tm_inter_fr.dim?x=inter&l=fr

3) Mise en place des composants

Créez une nouvelle application, et posez un composant "TIdFTP" (onglet "Indy Clients") sur votre fiche. Vous voyez que par défaut le n° de port est réglé sur 21.



Par défaut, le composant TIdFTP prend le contrôle de l'application entière quand il est en action, ou plutot quand on lui soumet une commande. Pour plus de confort, nous préfèrerons pouvoir utiliser l'application pendant les transferts. Pour cela, il nous faut placer un composant TIdAntiFreeze (onglet "Indy Misc"). Réglez sa propriété IdleTimeOut sur "50", et enfin sa proprité OnlyWhenIdle sur "False".



Posez aussi sur votre fiche un TGroupBox dont le Caption sera "Connexion Serveur", 3 TLabel, 3 TEdit, et 2 boutons. Le premier TLabel aura pour Caption "Serveur", le 2°, "Nom d'utilisateur" et le 3°, "Mot De Passe". Les 3 TEdit auront pour Text "ftp.borland.com", "anonymous", et "mail@server.com". Le 3° TEdit aura pour PassWordChar l'étoile "*". Enfin, les 2 boutons auront pour texte "Connecter" et "Déconnecter".



Posez à présent sur votre fiche un autre TGroupBox dont le Caption sera "Gestion Fichiers", 2 TLabel (dont les Captions seront "Répertoire Distant" et "Fichier Sélectionné"), un TEdit avec la propriété Ctl3D sur "false", la propriété ReadOnly sur "true" et le Text sur "/", et un TMemo vide avec la propriété Ctl3D sur "false", la propriété ReadOnly sur "true" , et la propriété WordWrap sur "false" et videz le texte de la propriété Lines. Posez un TSpeedButton à côté du TEdit, avec la propriété Flat sur "true", et une icône de flèche vers le haut, qui servira à revenir au répertoire parent. Enfin, posez un autre TSpeedButton à côté du TMemo, avec une icône d'annulation et sa propriété Flat sur "true", ainsique sa propriété Visible sur true, qui servira de bouton d'annulation du téléchargement...



Le FTP à la particularité de donner de nombreuses informations d'état sur les commandes que nous lui envoyons. Pour les afficher, posez un troisème TGroupBox sur votre fiche, dont le Caption sera "Etat", avec un TMemo vide à l'intérieur, dont la propriété ReadOnly sera sur "true". Ce TMemo servira à afficher les informations de connexion, et les résultats des opérations sur les fichiers.



Enfin, la finalité du protocole FTP est la gestion de fichiers. Pour afficher la liste des fichiers sur le serveur, placez un quatrième TGroupBox sur votre fiche, dont le Caption sera "Liste des Fichiers", et mettez un TListBox à l'intérieur.



Bien sûr, si nous executons le projet à ce stade, rien ne se passera... nous allons donc établir la connexion. Mais avant cela, rappelons-nous que nous avons posé un composant TIdAntiFreeze pour pouvoir continuer à manipuler l'application pendant les transferts. Seulement voilà, le composant FTP ne peut gérer qu'un transfert à la fois. Pour éviter de cliquer par mégarde sur un bouton de commande pendant un transfert, nous allons écrire une procédure qui désactivera tous les boutons pendant les transferts:

Activation/Désactivation des fonctions

procedure TForm1.EnableControls(Enable: boolean);
begin
  Button1.Enabled := Enable;
  Button2.Enabled := Enable;
  SpeedButton1.Enabled := Enable;
  ListBox1.Enabled := Enable;
end;



Maintenant, nous pouvons nous connecter en étant sûr d'éviter les problèmes. Pour se connecter, il faut choisir un serveur, et donner son nom d'utilisateur et son mot de passe. Mettez le code suivant dans l'évennement OnClick du bouton "Connecter":

Connexion

procedure TForm1.Button1Click(Sender: TObject);
begin
  if IdFTP1.Connected then IdFTP1.Disconnect;
  try
    EnableControls(false);
    IdFTP1.Host := Edit1.Text;
    IdFTP1.User := Edit2.Text;
    IdFTP1.Password := Edit3.Text;
    IdFTP1.Connect;
  finally
    EnableControls(true);
  end;
end;



et le code suivant dans l'évennement OnClick du bouton "Déconnecter" :

Déconnexion

procedure TForm1.Button2Click(Sender: TObject);
begin
  if IdFTP1.Connected then IdFTP1.Disconnect;
end;



Jusque là, rien de sorcier. Si vous exécutez le projet à ce stade, quand vous vous connecterez, vous n'aurez aucun signe pour savoir si votre connexion a réussi ou pas, et d'ailleurs vous ne saurez rien du tout. C'est pour cette raison que nous avons placé un TMemo pour afficher l'état de la connexion. Sélectionnez le composant TIdFTP, et mettez le code suivant dans l'évennement OnStatus:

Affichage du Status

procedure TForm1.IdFTP1Status(axSender: TObject; const axStatus: TIdStatus;
  const asStatusText: String);
begin
  Memo2.Lines.Add(asStatusText);
end;



A présent, si vous vous connectez et que vous lancez le projet, vous aurez déjà des informations sur la connexion quand vous l'établirez. Mais bon, l'intérêt est quand même assez limité, vu que la raison d'être du protocole FTP est la gestion des fichiers. Il est donc temps de s'y attaquer.

Rajoutez la ligne suivante à la fin du code de l'évennement OnClick du bouton "Connecter", juste avant le finally. Le try..except est utilisé ici pour éviter d'afficher les messages d'erreurs en cas d'interruption de transfert:

A rajouter dans le code de connexion

  try IdFTP1.List(ListBox1.Items); except; end;



et mettez le code suivant dans l'évennement OnDisconnected du TIdFTP:

Réaction à la déconnexion

procedure TForm1.IdFTP1Disconnected(Sender: TObject);
begin
  ListBox1.Clear;
  EnableControls(true);
end;



A présent, si vous lancez le projet, lors de votre connexion, vous obtiendrez la liste des fichiers et dossiers situés à la racine du serveur distant. Malheureusement, pour l'instant, il est impossible de changer de répertoire, on est cantonné à cette liste, assez complexe par ailleurs, qui peut se présenter sous les formes suivantes:

exemple de résultat sur un serveur UNIX/ext2fs

drwx------  2   ftpuser  ftpusers  512   Nov 23 1998    lost+found
drwxr-xr-x  19  ftpuser  ftpusers  1024  Jun 5 14:17    pub
lrwxrwxrwx  1   root     root      14    Jun 25 07:54   DIRS.byname -> ../DIRS.byname



exemple de résultat sur un serveur Windows

05-30-01  10:01AM   <DIR>     demos
04-04-00  02:15PM   2733901   force_commander.mov



Ce sont des entrées dont les champs sont séparés par des espaces. Il est important de bien comprendre la signification de chaque champ, pour pouvoir continuer notre programme. Voici donc leur description.

4) Entrées ext2fs

drwx------  2   ftpuser  ftpusers  512   Nov 23 1998    lost+found
drwxr-xr-x  19  ftpuser  ftpusers  1024  Jun 5 14:17    pub
lrwxrwxrwx  1   root     root      14    Jun 25 07:54   DIRS.byname -> ../DIRS.byname



  • Le premier champ d'une entrée ext2fs est constitué de 10 caractères, qui désignent les autorisations pour les fichiers, plus les attributs:

    le premier caractère peut être soit
    a) un tiret "-", l'entrée correspondra à un fichier
    b) un d, l'entrée correspondra à un répertoire (directory)
    c) un l, l'entrée correspondra à un lien symbolique (link)

    En effet, sous Unix, on peut établir des liens symboliques pour n'importe quel fichier ou répértoire, c'est à dire qu'on peut créer un fichier virtuel (le lien), qui, lorsqu'on l'ouvrira, pointera vers le fichier réel (la cible), au contraire de Windows, où on ne peut établir des liens aussi poussés. Par exemple, si sous Windows vous créez un raccourci dans le répertoire C:\ vers le répertoire C:\windows\system, et que vous nommez ce raccourci "lien", vous ne pourrez pas utiliser la commande
    "copy c:\netlog.txt lien\netlog.txt"
    et vous obtiendrez une erreur, car "lien" n'est pas un vrai lien symbolique, alors que sous Linux les liens symboliques permettent ce genre de manipulation


  • ensuite, nous avons une chaîne de type rwxrwxrwx, en fait constituée de 3 groupes rwx, qui correspondent à des autorisations d'accès aux fichiers. Si une lettre est remplacée par un tiret "-", c'est que l'autorisation n'a pas été donnée.
    Voici la désignation des trois lettres rwx de chaque groupe:
    a) La lettre r désigne une autorisation de lecture (read)
    b) La lettre w désigne une autorisation d'écriture (write)
    c) La lettre x désigne une autorisation d'exécution (eXecute) du fichier

    maintenant, la désignation des 3 groupes:
    a) Le groupe de gauche concerne le propriétaire du fichier (user)
    b) Le groupe du milieu concerne les utilisateurs appartenant au même groupe que le propriétaire du fichier (group)
    c) Le groupe de droite concerne tous les autres utilisateurs (other)

    Ainsi, sur l'exemple donné ci-dessus, le répertoire lost+found n'est accessible que par son créateur, il nous est même impossible de regarde le contenu, tandis que le répertoire pub est accessible par tout le monde en lecture.
  • Le 2° champ désigne le nombre de liens à ce fichier.
  • Le 3° champ désigne le propriétaire (user) du fichier.

  • Le 4° champ désigne le groupe (group) du fichier.

  • Le 5° champ désigne la taille en octets du fichier.

  • Le 6° champ désigne la date de dernière modification, constituée de 3 champs séparés par des espaces:
    a) le mois de modification
    b) le jour de modification
    c) soit l'heure au format HH:MM, soit l'année au format AAAA
  • Le 7° et dernier champ désigne le nom du fichier.



Les entrées correspondant à un lien sont légèrement différentes:

lrwxrwxrwx  1   root     root      14    Jun 25 07:54   DIRS.byname -> ../DIRS.byname



Les 6 premières entrées sont identiques aux entrées standard ext2fs, mais la dernière entrée est constituée de 3 champs, séparés par des espaces:
a) le nom du lien
b) la chaîne "->"
c) la cible du lien

A noter : la cible du lien n'existe pas forcément, tout comme les liens sur les pages HTML, on peut mettre n'importe quel texte ne comportant pas d'espace dans la cible...

5) Entrées Windows

05-30-01  10:01AM   <DIR>     demos
04-04-00  02:15PM   2733901   force_commander.mov



Les entrées Windows, qui utilisent la FAT, sont constituées de 4 champs:
a) la date de modification, au format MM:JJ:AA
b) l'heure de modification, au format HH:MM en mode 12 heure, suivi de AM ou PM selon
c) la chaîne <DIR> s'il s'agit d'un répertoire
ou la taille en octets s'il s'agit d'un fichier
d) le nom de répertoire ou de fichier, qui peut contenir des espaces...

Il n'existe pas de liens symboliques ni de droits d'accès sur les serveurs Windows


6) Détermination du type de serveur

Maintenant que nous connaissons la signification complète des résultats renvoyés par la commande List, nous pouvons analyser les entrées pour naviguer parmi les répertoires. Comme les entrées d'un serveur Windows sont très différentes des entrées d'un serveur UNIX, il serait judicieux de déterminer à quel type de serveur on se connecte.

Définissons tout d'abord un type qui servira à choisir le serveur, ainsi qu'une variable globale de ce type:

Type de serveur FTP

type
  TFTPServerType = (ftpUnix, ftpWindows);

var 
  FTPServerType: TFTPServerType;



Le seul moyen que nous avons pour l'instant de déterminer le type de serveur est de regarder le nombre d'espaces dans les entrées: un serveur Windows en génère 3, alors qu'un serveur Unix en génère 8. Par conséquent, on peut dire qu'un serveur dont les entrées contiennent plus de 4 espaces est un serveur Unix. Écrivons la procédure correspondante:

Détermination du type de serveur

procedure TForm1.GetServerType(const ServerEntry: string);
var
  s: string;
begin
  s := Trim(ServerEntry);
  s := Trim(Copy(s, Pos(' ', s) + 1, Length(s)));
  s := Trim(Copy(s, Pos(' ', s) + 1, Length(s)));
  s := Trim(Copy(s, Pos(' ', s) + 1, Length(s)));
  FTPServerType := TFTPServerType(Pos(' ', s) = 0);
end;



Enfin, pour déterminer le type de serveur, nous ne pouvons malheureusement pas choisir la première ou la dernière entrée car certains serveurs ajoutent des commentaires en début et/ou en fin de liste, ce qui empêche d'analyser ces commentaires comme des entrées. Nous analyserons donc les entrées à chaque clic...


7) Gestion des répertoires

Il nous faut d'abord écrire une fonction qui détermine si une entrée est un répertoire, une 2° fonction qui détermine si on a accès à ce répertoire ou pas, et enfin une 3° fonction qui détermine le nom du fichier/répertoire correspondant à l'entrée. Comme les entrées d'un serveur Unix sont différentes que celles d'un serveur Windows, il faut écrire, pour chaque fonction, une version Unix et une version Windows, et enfin une fonction qui utilise la bonne selon le type de serveur:

Gestion des répertoires

function IsFatDir(const FatEntry: string): boolean;
var
  s: string;
begin
  s := Trim(FatEntry);
  s := Trim(Copy(s, Pos(' ', s) + 1, Length(s)));
  s := Trim(Copy(s, Pos(' ', s) + 1, Length(s)));
  s := Trim(Copy(s, 1, Pos(' ', s) - 1));
  Result := UpperCase(s) = '<DIR>';
end;

function IsDir(const Entry: string): boolean;
begin
  if FTPServerType = ftpWindows then Result := IsFatDir(Entry)
  else Result := (Length(Trim(Entry)) > 0) and (LowerCase(Trim(Entry))[1] = 'd');
end;

function AccessAllowed(const Entry: string): boolean;
begin
  if FTPServerType = ftpWindows then Result := true
  else Result := (Length(Trim(Entry)) > 7) and (LowerCase(Trim(Entry))[8] = 'r');
end;

function GetFatEntryName(const FatEntry: string): string;
begin
  Result := Trim(FatEntry);
  Result := Trim(Copy(Result, Pos(' ', Result) + 1, Length(Result)));
  Result := Trim(Copy(Result, Pos(' ', Result) + 1, Length(Result)));
  Result := Trim(Copy(Result, Pos(' ', Result) + 1, Length(Result)));
end;

function GetExt2EntryName(const Ext2Entry: string): string;
begin
  Result := Trim(Ext2Entry);
  Result := Trim(Copy(Result, Pos(' ', Result) + 1, Length(Result)));
  Result := Trim(Copy(Result, Pos(' ', Result) + 1, Length(Result)));
  Result := Trim(Copy(Result, Pos(' ', Result) + 1, Length(Result)));
  Result := Trim(Copy(Result, Pos(' ', Result) + 1, Length(Result)));
  Result := Trim(Copy(Result, Pos(' ', Result) + 1, Length(Result)));
  Result := Trim(Copy(Result, Pos(' ', Result) + 1, Length(Result)));
  Result := Trim(Copy(Result, Pos(' ', Result) + 1, Length(Result)));
  Result := Trim(Copy(Result, Pos(' ', Result) + 1, Length(Result)));
end;

function GetEntryName(const Entry: string): string;
begin
  if FTPServerType = ftpWindows then Result := GetFatEntryName(Entry)
  else Result := GetExt2EntryName(Entry);
end;




Avec ces 3 fonctions, nous allons pouvoir déterminer si une entrée est un répertoire, si on y a accès, et le cas échéant, nous pourrons entrer dans le répertoire. Si nous ne vérifions pas les droits d'accès sur un serveur Unix avant d'accéder à un répertoire, nous aurons droit à un joli message d'erreur:



Sélectionnez la ListBox, et entrez le code suivant dans l'évennement OnDblClick. Remarquez qu'on analyse le type de serveur au double clic sur une entrée:

Double Clic dans la ListBox

procedure TForm1.ListBox1DblClick(Sender: TObject);
var
  Entry: string;
begin
  if Listbox1.Count > 0 then
    begin
      Entry := ListBox1.Items[ListBox1.ItemIndex];
      GetServerType(Entry);
      ChangeDir(Entry);
    end;
end;



et définissez la procédure suivante:

Changement de répertoire

procedure TForm1.ChangeDir(const Entry: string);
var
  Name: string;
begin
  if IsDir(Entry) then
    begin
      if AccessAllowed(Entry) then
        begin
          try
            EnableControls(false);
            Name := GetEntryName(Entry);
            Edit4.Text := Edit4.Text + Name + '/';
            IdFTP1.ChangeDir(Edit4.Text);
            try IdFTP1.List(ListBox1.Items); except; end;
          finally
            EnableControls(true);
          end;
        end
      else
        begin
          Memo2.Lines.Add('Accès non autorisé au répertoire ' + Name)
        end;
    end
end;



pour revenir à un répertoire parent, il nous faut une fonction qui analyse la chaîne et qui renvoie le répertoire précédent:

Répertoire parent

function GetPrevLevel(const FolderName: string): string;
var
  a: integer;
  s, t: string;
begin
  a := Pos('/', FolderName);
  if a = 0 then Result := FolderName
  else
    begin
      if FolderName[Length(FolderName)] = '/' then
        begin
          t := Copy(FolderName, 1, Length(FolderName) - 1);
          a := Pos('/', t);
        end
      else t := FolderName;
      s := '';
      while a > 0 do
        begin
          s := s + Copy(t, 1, a - 1) + '/';
          t := Copy(t, a + 1, Length(t));
          a := Pos('/', t);
        end;
      Result := Copy(s, 1, Length(S) - 1) + '/';
    end;
end;



Sélectionnez le TSpeedButton, et mettez le code suivant dans l'évennement OnClick:

Remonter au répertoire parent

procedure TForm1.SpeedButton1Click(Sender: TObject);
begin
  if not IdFTP1.Connected then Exit;
  if Length(Edit4.Text) = 1 then Memo2.Lines.Add('Vous êtes déjà au répertoire racine')
  else
    begin
      try
        EnableControls(false);
        Edit4.Text := GetPrevLevel(Edit4.Text);
        IdFTP1.ChangeDir(Edit4.Text);
        try IdFTP1.List(ListBox1.Items); except; end;
      finally
        EnableControls(true);
      end;
    end;
end;



Ajoutez la ligne suivante à l'évennement OnDisconnected du TIdFTP:

Code à rajouter à l'évennement OnDisconnected

Edit4.Text := '/';




8) Gestion des fichiers

Bon, la gestion des répertoires standard est complète, mais il reste encore à gérer les liens, qui peuvent désigner soit un fichier, soit un répertoire. Avant ça, occupons nous de la gestion des fichiers standard...

Tout d'abord, écrivons une fonction qui nous permettra de déterminer si une entrée correspond à un fichier:


Fichier ou pas ?

function  IsFile(const Entry: string): boolean;
begin
  if FTPServerType = ftpWindows then Result := not isFatDir(Entry)
  else Result := (Length(Trim(Entry)) > 0) and (Trim(Entry)[1] = '-');
end;



Maintenant, nous sommes prêts à récupérer des fichiers depuis un serveur distant. Placez simplement sur votre fiche un composant TSaveDialog. Nous voulons également afficher la progression et la vitesse de téléchargement pendant le téléchargement, nous aurons donc besoin d'une variable globale FileSize, de type integer, d'une variable FileName, de type string, et enfin d'une variable STime de type TDateTime:

3 variables globales

var
  FileSize: integer;
  FileName: string;
  STime: TDateTime;




Sélectionnez maintenant le composant TListBox et modifiez le code de l'évennement OnDblClick pour obtenir le code suivant:

Double Clic sur la ListBox

procedure TForm1.ListBox1DblClick(Sender: TObject);
var
  Entry: string;
begin
  if Listbox1.Count > 0 then
    begin
      Entry := ListBox1.Items[ListBox1.ItemIndex];
      GetServerType(Entry); 
      ChangeDir(Entry);
      DownLoad(Entry);
    end;
end;



et définissez la procédure suivante:

Téléchargement

procedure TForm1.Download(const Entry: string);
var
  Name: string;
begin
  if IsFile(Entry) then
    begin
      if AccessAllowed(Entry) then
        begin
          Name := GetEntryName(Entry);
          FileName := Name;
          SaveDialog1.FileName := Name;
          try
            if SaveDialog1.Execute then
              begin
                EnableControls(false);
                IdFTP1.TransferType := ftBinary;
                FileSize := IdFTP1.Size(Name);
                try
                  IdFTP1.Get(Name, ExpandFileName(SaveDialog1.FileName), true);
                except
                  Memo2.Lines.Add('Echec lors du transfert');
                end;
              end;
          finally
            EnableControls(true);
          end;
        end
        else
          begin
            Memo2.Lines.Add('Accès non autorisé au fichier ' +  Name)
          end;
    end;
end;



Bon, maintenant, on peut télécharger un fichier sans problème, mais il reste encore un problème: si on télécharge un gros fichier, on risque de s'ennuyer ferme devant l'application, sans savoir si elle a planté ou pas... Pour ça, nous allons afficher la vitesse de réception ainsi que la quantité téléchargée du fichier en cours.

Lors du début du transfert, l'évennement OnWorkBegin du TIdFTP est déclenché. Pendant le transfert, l'évennement OnWork est régulièrement déclenché. A la fin du transfert, l'évennement OnWorkEnd est déclenché. Pour savoir si nous sommes en cours de transfert ou pas, nous allons définir une variable globale TransferingData de type booléen. Pour voir si on veut annuler le transfert, nous allons déclarer une variable globale AbortTransfer de type booléen:

variables globales

var
  TransferringData: boolean;
  AbortTransfer: boolean;




Mettez le code suivant dans l'évennement OnWorkBegin du TIdFTP:

OnWorkBegin

procedure TForm1.IdFTP1WorkBegin(Sender: TObject; AWorkMode: TWorkMode;
  const AWorkCountMax: Integer)

3 requête(s) SQL executée(s) en 0.089 Secs - Temps total de génération de la page : 0.095 Secs