Delphi 7: Réaliser un Client FTP à l'aide des composants Indy
Télécharger les sources du projet (5.24 ko)
Delphi 7 est fourni avec les composants Indy. Voyons comment les utiliser pour mettre en oeuvre un client FTP. Ce document est la mise à jour du document précédent qui concernait Delphi 6, vu que les composants Indy ont évolué depuis.
1) 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 suivate:
http://www.eisti.fr/res/res/rfc959/959tm_inter_fr.dim?x=inter&l=fr
2) 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 Divers"). 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 contrôles
procedure TForm1.EnableControls(Enable: boolean); begin Button1.Enabled := Enable; Button2.Enabled := Enable and IdFTP1.Connected; SpeedButton1.Enabled := Enable and IdFTP1.Connected; ListBox1.Enabled := Enable and IdFTP1.Connected; 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.UserName := 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:
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
if idFTP1.Connected then
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.
3) 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:
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
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.
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
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...
4) 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
5) 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...
6) 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 := '/';
7) 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 (vous aurez besoin d'utiliser l'unité IdFTPCommon) :
Téléchargement
...
uses
Windows, ..., IdFtpCommon;
...
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;
Mette
3 requête(s) SQL executée(s) en 0.001 Secs - Temps total de génération de la page : 0.007 Secs
