1. Introduction
En programmation, la sécurité des opérations sur les fichiers ne concerne pas seulement la protection contre les virus et les hackers, mais aussi la gestion correcte des erreurs, des verrous, des permissions, des chemins incorrects et autres pièges. Une approche négligente peut conduire à une perte de données, au blocage du programme, à des bugs étranges ou à la tristement célèbre exception IOException.
Voici des situations classiques qu'on veut savoir gérer :
- Le fichier n'existe pas alors qu'on essaie de le lire (ou inversement : il existe déjà alors qu'on veut créer seulement s'il n'existe pas).
- Le fichier est ouvert par un autre programme et est verrouillé.
- L'utilisateur n'a pas les droits de lecture ou d'écriture sur ce fichier ou dossier.
- Le chemin du fichier est erroné ou contient des caractères interdits.
- L'opération est interrompue de façon inattendue (par exemple, disque plein).
- Mauvaise pratique : laisser des fichiers ouverts, ne pas fermer les streams.
Heureusement, .NET fournit tous les outils pour résoudre ces problèmes. Ils sont simples — mais, comme avec les ceintures de sécurité, l'important est de penser à les attacher.
2. Principes de base pour travailler en sécurité avec des fichiers
Vérifiez l'existence du fichier et les droits à l'avance
Avant de lire un fichier, vérifiez qu'il existe (par exemple avec File.Exists), surtout si le chemin vient d'un utilisateur :
string path = "test.txt";
if (!File.Exists(path))
{
Console.WriteLine("Erreur : fichier non trouvé.");
return;
}
Avant d'écrire, assurez-vous que le répertoire existe et que vous avez les droits d'écriture (ou remontez l'erreur à un niveau supérieur).
Ne laissez jamais les streams ouverts
Utilisez le mot-clé using pour fermer automatiquement les streams — c'est la meilleure pratique :
using var writer = new StreamWriter("output.txt");
writer.WriteLine("Hello, files!");
// Ici le fichier est déjà fermé, même si une erreur est survenue !
Cela protège contre les verrous « bloqués » et les fuites de ressources.
Attrapez toujours les exceptions
Toute opération sur les fichiers peut lancer une exception. Même si le fichier était là il y a une seconde — il peut être supprimé/déplacé au moment de l'accès. Utilisons la construction try-catch :
try
{
using var reader = new StreamReader("data.txt");
string line = reader.ReadLine();
Console.WriteLine(line);
}
catch (FileNotFoundException)
{
Console.WriteLine("Fichier non trouvé.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Pas d'accès au fichier.");
}
catch (IOException ex)
{
Console.WriteLine($"Erreur d'entrée-sortie : {ex.Message}");
}
Ne faites pas confiance à l'entrée utilisateur
Si le chemin du fichier est fourni par l'utilisateur, il peut être mal saisi (ou on peut tenter de faire planter votre programme). Il est recommandé de valider le chemin (voir Path.GetInvalidPathChars()) :
try
{
string userPath = Console.ReadLine()!;
if (string.IsNullOrWhiteSpace(userPath))
{
Console.WriteLine("Le chemin ne peut pas être vide !");
return;
}
// En plus : vérifier les caractères interdits
foreach (char c in Path.GetInvalidPathChars())
if (userPath.Contains(c))
{
Console.WriteLine("Le chemin contient des caractères interdits.");
return;
}
// Ensuite travailler en sécurité avec le fichier
}
catch (Exception ex)
{
Console.WriteLine("Erreur de validation du chemin : " + ex.Message);
}
3. Exemple pratique : lire un fichier « comme un pro »
Améliorons un peu notre projet d'étude. Imaginons qu'on doive lire un fichier dont le nom est saisi par l'utilisateur et afficher son contenu à l'écran. Le tout avec gestion des erreurs et prise en compte de l'encodage.
Console.Write("Entrez le chemin du fichier : ");
string? path = Console.ReadLine();
// Validation du chemin
if (string.IsNullOrWhiteSpace(path))
{
Console.WriteLine("Le chemin ne peut pas être vide !");
return;
}
foreach (char c in Path.GetInvalidPathChars())
if (path.Contains(c))
{
Console.WriteLine("Le chemin contient des caractères interdits.");
return;
}
// Essayons de lire le fichier
try
{
if (!File.Exists(path))
{
Console.WriteLine("Fichier non trouvé.");
return;
}
// Spécifier explicitement l'encodage (par exemple UTF-8)
using var reader = new StreamReader(path, Encoding.UTF8);
string content = reader.ReadToEnd();
Console.WriteLine("Contenu du fichier :");
Console.WriteLine(content);
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Pas de droits pour lire le fichier.");
}
catch (IOException ex)
{
Console.WriteLine("Erreur d'entrée-sortie : " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Erreur inattendue : " + ex.Message);
}
Notez que ce code ne plantera pas si le fichier disparaît entre la saisie du nom et la lecture : l'exception sera attrapée. Le stream se fermera automatiquement en sortant du bloc using — même si une erreur survient.
4. Fichiers en écriture : éviter la perte de données
Quand on ouvre un fichier en écriture, surtout en mode écrasement (false en deuxième paramètre du constructeur StreamWriter), il y a un risque d'effacer des données importantes. Voici quelques conseils :
Vérifiez que vous n'écrasez pas un fichier existant
Parfois il est utile de demander à l'utilisateur si le fichier existe déjà :
if (File.Exists(path))
{
Console.WriteLine("Attention : le fichier existe déjà. Écraser ? (y/n)");
string answer = Console.ReadLine()!;
if (!answer.Equals("y", StringComparison.OrdinalIgnoreCase))
return;
}
Utilisez le mode « append » quand il le faut
using var writer = new StreamWriter("log.txt", append: true);
writer.WriteLine(DateTime.Now + ": nouvelle entrée dans le journal.");
Les anciennes informations ne disparaîtront pas.
5. Protection contre les races conditions et conflits
Parfois plusieurs programmes peuvent ouvrir le même fichier en même temps (par exemple votre client C# et Notepad++). Cela peut provoquer des erreurs. Par défaut StreamReader et StreamWriter utilisent des modes de partage basés sur FileShare — c'est-à-dire autoriser d'autres à lire ou interdire.
On peut contrôler ça explicitement :
using var stream = new FileStream("data.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream, Encoding.UTF8);
// Lecture...
- FileShare.Read : les autres peuvent seulement lire.
- FileShare.None : aucun partage — le fichier est entièrement « à vous ».
Si vous voulez que le fichier puisse être lu et écrit par différents programmes en même temps, utilisez le mode correspondant, mais soyez prudent : on fait ça seulement quand on comprend bien les conséquences.
6. Exceptions inattendues et quoi en faire
Même en respectant toutes les recommandations, des pannes peuvent survenir, liées par exemple à un disque plein, une clé USB défectueuse ou un virus. Voici quelques exceptions « exotiques » et comment les attraper :
- PathTooLongException — le chemin est trop long (plus de 260 caractères sur les anciennes versions de Windows).
- DirectoryNotFoundException — le répertoire spécifié est introuvable.
- DriveNotFoundException — par exemple si le chemin est "Z:\\file.txt" et que le lecteur Z n'existe pas.
- NotSupportedException — par exemple le chemin contient une combinaison non supportée.
Il est recommandé d'avoir des blocs séparés pour ces exceptions — au minimum les logger séparément.
7. Utiliser des fichiers temporaires pour une écriture atomique
Problème classique : on écrit un fichier et le programme plante au milieu — on obtient un fichier corrompu. Les applications pro utilisent souvent la stratégie d'écriture « atomique » :
- Écrire le contenu dans un fichier temporaire (par exemple "file.txt.tmp").
- Déplacer le fichier temporaire (opération souvent atomique au niveau du système de fichiers) par-dessus la cible (File.Replace).
- Le fichier ancien est soit complètement remplacé, soit reste inchangé — pas de données à moitié écrites.
Exemple :
string tempPath = path + ".tmp";
try
{
using var writer = new StreamWriter(tempPath, false, Encoding.UTF8);
// On écrit tout dans le fichier temporaire
writer.Write(contentForSave);
// Après un write réussi on remplace le fichier principal
File.Replace(tempPath, path, null); // Déplace le fichier temporaire en remplaçant la cible (opération atomique)
}
catch (Exception ex)
{
Console.WriteLine("Erreur lors de la sauvegarde du fichier : " + ex.Message);
// Il est souhaitable de supprimer tempPath si on n'en a pas besoin
}
Dans la vraie vie, beaucoup d'éditeurs et de suites bureautiques font ainsi pour garantir la consistance des données.
8. Utiliser des classes wrapper pour un accès sécurisé
.NET propose des méthodes utilitaires pour des opérations « sécurisées » sur les fichiers. Par exemple File.ReadAllText et File.WriteAllText ouvrent, lisent/écrivent et ferment automatiquement le fichier. Mais on les enveloppe quand même dans un try-catch :
try
{
string text = File.ReadAllText("settings.json", Encoding.UTF8);
// Travailler avec les données...
}
catch (Exception ex)
{
Console.WriteLine("Erreur lors de l'opération fichier : " + ex.Message);
}
Pour les gros fichiers, utilisez des streams et lisez par morceaux pour ne pas tout charger en mémoire.
GO TO FULL VERSION