Cours de Python / Copier des fichiers avec dry-run et contrôle des collisions
Pour automatiser une copie de fichiers, séparez la simulation, ou dry-run, de l’exécution réelle. Cet exercice affiche d’abord les fichiers texte concernés, puis ne copie qu’avec --apply et refuse de remplacer une destination existante.
Préparer un dossier d’exercice et un fichier source
Travaillez dans un nouveau dossier réservé à cet exercice. Le programme utilise les chemins relatifs copy-demo/in et copy-demo/out sous le dossier de travail du terminal, sans déplacer ni supprimer les sources. Vérifiez ce dossier avec Get-Location dans PowerShell ou pwd dans un shell macOS ou Ubuntu avant l’exécution. Le cours sur les chemins avec pathlib explique la différence entre emplacement du script et dossier de travail.
La préparation écrit volontairement un petit fichier. Elle est distincte du dry-run de l’outil de copie, qui ne crée ni dossier de sortie ni fichier. Enregistrez le programme Python suivant en UTF-8 sous setup_copy.py et lancez python setup_copy.py une seule fois dans le dossier d’exercice, avec l’environnement virtuel du projet sélectionné. Sous macOS ou Ubuntu hors environnement virtuel, utilisez python3 si c’est votre commande d’interpréteur.
from pathlib import Path
source = Path("copy-demo/in")
source.mkdir(parents=True, exist_ok=True)
with (source / "note.txt").open("x", encoding="utf-8") as f:
f.write("demo\n")
print("sample ready")
Le message sample ready confirme la préparation. Vous devez trouver copy-demo/in/note.txt contenant demo et un saut de ligne. Le mode x refuse une seconde création du même fichier : si vous relancez ce script, FileExistsError doit préserver le contenu déjà présent. Ne contournez pas ce contrôle en remplaçant x par w ; utilisez un nouveau dossier d’exercice si vous voulez recommencer sans examiner les fichiers existants.
Inspecter la simulation sans copier
Enregistrez le programme complet ci-dessous dans safe_copy.py, à côté de setup_copy.py. Il ne demande aucune dépendance externe. Après la préparation, lancez python safe_copy.py depuis le même dossier, ou python3 safe_copy.py si vous avez utilisé python3.
import argparse
import hashlib
import shutil
from pathlib import Path
parser = argparse.ArgumentParser()
parser.add_argument("--apply", action="store_true")
args = parser.parse_args()
source, target = Path("copy-demo/in"), Path("copy-demo/out")
if not source.is_dir():
parser.error("prepare copy-demo/in first")
files = sorted(p for p in source.glob("*.txt") if p.is_file() and not p.is_symlink())
for src in files:
dst = target / src.name
if dst.exists() or dst.is_symlink():
raise FileExistsError(dst)
print("copy", src.name)
if args.apply:
target.mkdir(parents=True, exist_ok=True)
with src.open("rb") as incoming, dst.open("xb") as outgoing:
shutil.copyfileobj(incoming, outgoing)
if hashlib.sha256(src.read_bytes()).digest() != hashlib.sha256(dst.read_bytes()).digest():
raise RuntimeError("copy verification failed")
print("applied" if args.apply else "dry-run")
Résultat attendu avant toute copie réelle :
copy note.txt
dry-run
L’outil affiche les noms des fichiers sélectionnés sous copy-demo/in, pas une liste de chemins absolus. Il recherche seulement les entrées *.txt de ce niveau, garde les fichiers ordinaires et exclut les entrées qui sont des liens symboliques. Il n’explore pas récursivement les sous-dossiers. Vérifiez donc le dossier de travail et les deux emplacements fixes en plus des noms affichés.
Sans --apply, le bloc d’écriture n’est pas exécuté. Vous pouvez relancer cette simulation et vérifier que copy-demo/out n’existe toujours pas. Si aucune source .txt n’est sélectionnée, seul dry-run est affiché : ce résultat n’indique pas qu’un fichier a été copié. Une source absente provoque au contraire un message demandant de préparer copy-demo/in.
Appliquer la copie et vérifier le contenu
Après examen de la simulation, lancez :
python safe_copy.py --apply
Avec python3, utilisez le même nom de commande que précédemment, en conservant --apply. La première exécution affiche copy note.txt puis applied et crée copy-demo/out/note.txt. La source reste dans in. L’ouverture de la destination avec xb crée un fichier binaire exclusivement : une destination déjà présente doit provoquer un échec au lieu d’être tronquée. Les modes d’ouverture de Python précisent cette règle.
shutil.copyfileobj transfère les octets entre les flux ouverts. Le bloc with ferme les deux fichiers avant que le programme relise la destination pour la vérification. Cette fermeture est importante : la documentation de copyfileobj ne garantit pas à elle seule que tous les tampons sont vidés à son retour.
Le programme compare ensuite des empreintes SHA-256 du contenu source et destination. SHA signifie Secure Hash Algorithm ; l’empreinte donne ici un contrôle de concordance des octets lus. Elle ne contrôle ni les autorisations, ni toutes les métadonnées, ni l’origine digne de confiance d’un fichier. La documentation de hashlib décrit le calcul des empreintes.
Relancez la même commande --apply : elle doit s’arrêter avec FileExistsError et laisser le résultat précédent intact. Le dry-run lui-même échoue aussi sur une destination existante, puisque la détection de collision précède le choix d’écrire. Cette politique évite de présenter une nouvelle copie comme applicable alors que son nom est déjà occupé.
Comprendre les échecs partiels avant d’étendre l’outil
Le traitement est séquentiel, fichier par fichier, et non une transaction globale. Si un premier fichier est copié puis qu’un deuxième rencontre une collision, le premier résultat reste présent. De même, une erreur pendant le transfert peut laisser une destination partielle. Les règles de propagation des exceptions permettent de repérer le point d’arrêt, sans supposer que toutes les opérations antérieures ont été annulées.
Avant une reprise, inspectez les chemins résolus, les noms et le contenu des résultats. Préférez pour l’exercice un nouveau dossier vide plutôt qu’une suppression automatique destinée à faire disparaître l’erreur. Si vous nettoyez manuellement, assurez-vous que le chemin absolu vise bien les seuls fichiers d’essai sous copy-demo et conservez ce qui vous est encore utile.
La vérification par read_bytes charge chaque petit fichier entier en mémoire ; de grands fichiers demanderaient un calcul par blocs. Le filtrage des liens porte sur les entrées trouvées, pas sur une défense complète contre des répertoires parents remplacés simultanément par un utilisateur non fiable. Le programme ne fournit pas non plus un instantané d’une source modifiée pendant la copie.
Pour une utilisation plus large, définissez d’abord le périmètre réel, les collisions possibles, les sauvegardes, les permissions et la reprise après échec. Ajouter un déplacement ou une suppression modifierait la politique de conservation des originaux : cela demande une conception distincte, pas simplement une option supplémentaire à ce script de copie.




