I. Historique

L'idée m'est venue lorsque j'ai entrepris de regrouper et de classer tous les bouts de code que j'avais ramassé au fils des ans. Dans la catégorie "Copie de Fichier", j'avais différentes possibilités. Certaines routines étaient désuètes et voici en gros les étapes classiques pour effectuer une copie de fichier... comme dans le bon vieux temps...

  1. Allocation d'un buffer de copie
  2. Ouverture du fichier source (CreateFile)
  3. Création du fichier de destination (CreateFile)
  4. Tant que le fichier source n'est pas tout lu, répète:
     - Lecture du fichier source dans le buffer (ReadFile)
     - Écriture du buffer dans le fichier destination (WriteFile)
     - Donner un compte rendu à l'utilisateur
  5. Fermeture fichier source (CloseHandle)
  6. Fermeture fichier destination (CloseHandle)
  7. désallouer le buffer de copie

On peut aujourd'hui remplacer cet algorithme en utilisant directement la fonction API de MS-Windows CopyFileEx. J'étais sur le point de la supprimer lorsque je me suis souvenu de la documentation de l'API CreateFile. Depuis Windows NT, donc (avec Windows 2000 et XP aussi) on peut utiliser CreateFile pour obtenir un Handle sur un disque physique. Je me suis alors posé la question: "Et si j'essayais d'utiliser ReadFile et WriteFile avec un Handle de disque physique pour en faire une copie?"

Et bien ça fonctionne parfaitement!

II. Résumé rapide

Étant donné que nous allons copier un disque entier vers un autre disque, il est évident qu'il nous faut prendre quelques précautions. Il ne faut absolument pas permettre une écriture sur le disque pendant la copie. C'est ici que la fonction API DeviceIoControl intervient. Cette fonction nous permet de barrer un disque, et même de "démonter" des partitions (volumes) via les codes de contrôle FSCTL_LOCK_VOLUME et FSCTL_DISMOUNT_VOLUME.

Une fois que le disque source et que le disque de destination sont démontés, on effectue simplement la copie... comme si c'était un fichier... à l'aide de ReadFile et WriteFile. Un très gros fichier certes, mais ces fonctions font tout de même leur travail. Peu importe le nombre de partitions ou le type de partition (FAT, NTSF et autre) sur le disque source, la lecture complète du disque sera effectuée de façon transparente et les informations seront intégralement copiées sur le disque de destination.

III. En détail

Maintenant, regardons en détail comment obtenir un Handle sur un disque physique via CreateFile. extrait de http://msdn2.microsoft.com/en-us/library/aa363858.aspx

 
Sélectionnez

//Source: http://msdn2.microsoft.com/en-us/library/aa363858.aspx
HANDLE CreateFile(
    LPCTSTR lpFileName,  // pointeur vers un nom de fichier
    DWORD dwDesiredAccess,  // mode d'acces (Lecture-Ecriture) 
    DWORD dwShareMode,  // mode de partage 
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,	// pointeur de sécurité
    DWORD dwCreationDistribution,  // Comment on créer le fichier
    DWORD dwFlagsAndAttributes,  // Attributs du fichier
    HANDLE hTemplateFile  // handle vers un fichier pour la copie des attributs
   );	

La documentation de Microsoft à propos de CreateFile nous explique que nous devons bâtir le nom du fichier d'ouverture comme suit: "\\.\PHYSICALDRIVEx" où x est le numéro du disque. On peut facilement retrouver ce numéro en allant jeter un coup d'oeil à la console du Disque Manager [ou Gestionnaire de Disques] de Windows:

Image non disponible

Ensuite, le paramètre dwCreationDistribution doit avoir OPEN_EXISTING comme valeur et FILE_SHARE_WRITE doit être présent dans le paramètre dwShareMode. Voici donc la ligne qui donne accès au Handle du disque source dans la démo:

 
Sélectionnez

//== Essaie d'obtenir un Handle sur le Disque Physique choisi.... ==
SrcHFile := CreateFile(PChar('\\.\PHYSICALDRIVE' + intToStr(Integer(SelectedItem.Data)))
                       , GENERIC_READ
                       , FILE_SHARE_READ Or FILE_SHARE_WRITE
                       , Nil, OPEN_EXISTING, 0, 0);

Pour obtenir des Handles sur les volumes associés à chaque disque, Microsoft nous indique que le nom de fichier doit être construit comme suit: "\\.\x:" où x représente la lettre associée au volume désiré. Dans la démo, on peut voir les lignes suivantes:

 
Sélectionnez

For i := 0 To NbrVolume - 1 Do
Begin
  //== Construction du local path pour un volume exemple: '\\.\E:'
  VolumePath := '\\.\' + VolumeList[(i * 2) + 1] + ':';
  ArrayVolumeSrc[i] := CreateFile(PChar(VolumePath)
                       , GENERIC_READ
                       , FILE_SHARE_READ Or FILE_SHARE_WRITE
                       , Nil, OPEN_EXISTING, 0, 0);

Une fois les Handles obtenus, nous devons barrer les volumes l'aide de DeviceIoControl avec la commande FSCTL_LOCK_VOLUME. Une bonne raison de barrer le disque est de permettre à Windows de vider tous les buffers de la cache associés au volume. Un échec pour barrer un volume indique probablement qu'on fichier est actuellement ouvert sur ce dernier. On doit alors utiliser les grands moyens avec la fermeture complète du volume à l'aide de la commande FSCTL_DISMOUNT_VOLUME.

 
Sélectionnez

//== Impose une barrure sur le disque ==
If Not DeviceIoControl(ArrayVolumeSrc[i], FSCTL_LOCK_VOLUME, Nil, 0, Nil, 0, dw, Nil) Then
Begin
  //== On ne fait rien...on va Forcer la fermeture du volume de toute facon ==
End;
//== Force le fermeture du disque ==
If Not DeviceIoControl(ArrayVolumeSrc[i], FSCTL_DISMOUNT_VOLUME, Nil, 0, Nil, 0, dw, Nil) Then
Begin
  CloseHandle(ArrayVolumeSrc[i]);
  ArrayVolumeSrc[i] := INVALID_HANDLE_VALUE;
  DisplaySystemError;
End;

Une fois les mêmes étapes effectuées sur le disque de destination, nous sommes maintenant prêts à copier. Voici la boucle de copie dont les éléments de vérification et de compte rendu visuel pour l'utilisateur ont été retirés.

 
Sélectionnez

//== Ca nous prend un pointer sur un gros buffer ==
GetMem(Buf, BUF_SIZE);  //BUF_SIZE = 1024 * 512;
Try
  //== Lecture d'un premier morceau de disque...et oui...Comme un fichier! ==
  BytesRead := FileRead(SrcHFile, Buf^, BUF_SIZE);
  While BytesRead > 0 Do
  Begin
    // ... //
    //== On ecrit les données ==
    BytesWrite := FileWrite(DstHFile, Buf^, BytesRead);
    // ... //
    //== Ok, tout va bien, et on continue à lire notre fichier              ==
    //== (mais dans les faits c'est notre disque)
    BytesRead := FileRead(SrcHFile, Buf^, BUF_SIZE);
  End;
  //== Voilà! Tout le disque source est copié ==
  // ... //
Finally
  //== Liberation de la mémoire ==
  FreeMem(Buf);
End;

IV. Implication de la duplication des stations en réseau

Les stations sur lesquelles est installé le système d'exploitation Windows (NT, 2000, 2003 et XP) utilisent un numéro de sécurité (SID) comme identifiant unique. Le SID est composé de 96 bits qui est généré lorsqu'on procède à l'installation de Windows.

Lorsqu'on effectue la duplication d'un disque qui contient un système d'exploitation Windows, et ce dans le but d'obtenir 2 machines avec des configurations identiques, il est important de maintenir des SID différents pour les 2 machines. Une bonne façon de changer le SID d'une machine est d'utiliser l'utilitaire de Microsoft sysprep.exe. Cet outil n'est pas installé par défaut et il est sur le CD d'installation de XP dans le fichier de distribution \Support\Tools\Deploy.cab.

Des mises à jour de sysprep.exe existent dans les divers service pack de Windows. Il est essentiel de réinstaller le service pack de Windows après avoir installé Deploy.cab.

Sysprep.exe enlève toutes les informations relatives à l'identification unique du système d'exploitation, donc attention, il y aura effacement des comptes locaux. Lorsque sysprep.exe termine, l'ordinateur se ferme. Au redémarrage, une mini procédure d'installation est lancée... on demande la clef du CD, le nom de la machine, une détection des composants s'exécute etc. Par contre, tous les programmes qui étaient installés avant sysprep.exe fonctionnent correctement. Il faut savoir que cette mini procédure d'installation est très rapide si on la compare à un setup normal (peut-être 10 minutes) car il n'y a aucune copie/decompression de fichier à faire puisque tout est déjà en place.

Une autre façon de changer le SID d'une station de travail est d'utiliser l'utilitaire NewSid.Exe. Cet outil à été écrit par Mark Russinovich and Bryce Cogswell (site de Sysinternals).Ce site à été acquis par Microsoft en Juillet 2006. Malheureusement, cet outil n'est pas supporté par Microsoft et on doit donc l'utiliser à nos risques et périls. Voici le lien pour télécharger la version 4.10.

http://www.microsoft.com/technet/sysinternals/utilities/NewSid.mspx

V. Questions Reponses...

  • Est-ce que les disques USB sont traités de la même manière que les SATA/IDE/SCSI ?

     - Du moment que le disque obtient un numéro de disque physique par Windows, on peut utiliser cette technique de copie. J'ai déjà copié un SCSI vers un disque IDE, et un IDE vers un SATA. J'imagine aussi que s'il s'agit d'un RAID géré par une carte contrôleur, Windows de toute façon le voit comme un seul disque. On pourrait alors, avec cette méthode, copier le RAID0,1 ou 5 peu importe, sur un autre disque.

  • Est-ce que la copie d'une seule partition via cette méthode est adaptée ?

     - S'il s'agit de la première partition, et que l'on veut copier seulement la première partition on peut le faire sans problème. S'il s'agit de copier la première partition vers une deuxième partion sur le disque cible alors la réponse est non.

  • Les disques de destination doivent-ils être de même taille que les disques d'origines ?

     - Absolument pas. Par contre, on doit s'assurer que les partitions sur le disque source seront entièrement copiées sur la cible...sinon, gare aux problèmes... Je suppose ici un disque source de 80Go partionnée en 2 tels que la 1ère partition=20Go et 2ème partition=60Go (enfin le reste). Je peux facilement copier la première partition sur un disque de destination de 30Go. Je peux aussi copier les 2 partitions sur un disque de 120Go. Dans ce cas, à la fin, j'obtiens 1 partition de 20Go, 1 partition de 60Go et 40Go libre.

VI. Conclusion

Contrairement à cette démo qui fait une copie disque à disque, on pourrait sauvegarder les informations lues sur le disque source dans un simple fichier. Ce fichier nécessitera donc un autre support (disque, clé USB,..). Un petit algorithme de compression serait probablement ici bienvenu pour permettre au fichier image d'être plus petit que le disque source. Voici en gros comment il faudrait procéder avec la création d'un fichier image:

  1. Ouverture de la source (CreateFile() avec un handle de disque)
  2. Ouverture de la destination (CreateFile() avec un handle de fichier)
  3. Allocation d'un bloc de mémoire
  4. Tant que la totalité du disque source n'est pas lue, répète:
     - Lecture d'un bloc sur le disque. (ReadFile())
     - Compression du bloc.(Huffman, zlib, etc)
     - Écriture du bloc compressé dans un fichier. (WriteFile())
  5. Désallocation de mémoire
  6. Fermeture du handle fichier
  7. Fermeture du handle Disque

La relecture du fichier et sa décompression, le tout ré-écrit sur un disque, devrait en théorie nous redonner exactement le disque source.

J'ai personnellement sauvé quelques ordinateurs qui commençaient à donner des signes de faiblesse (surtout des serveurs) en les changeant de disque avec ce petit programme. J'ai même transféré des serveurs sur disque IDE vers des SATA en suivant une séquence un peu particulière qui déborde cependant ce sujet.

Alors Bonne Duplication!

VII. Code Source Delphi 5.0 et Executable

Requis: Windows 2000 SP2 ou plus (WMI est utilisé pour associer les numéros de disque physique et les partitions entre eux)

DupDisk.zip

VIII. Remerciments

Merci à Laurent Dardenne et à Pedro pour la relecture et les nombreux conseils.