1. Introducción
En programación, la seguridad al trabajar con archivos no solo significa defensa contra virus y hackers, sino también manejar correctamente errores, bloqueos, permisos, rutas inválidas y otros escollos. Un enfoque descuidado puede causar pérdida de datos, cuelgues de la aplicación, bugs extraños o la temida excepción IOException.
Aquí están las situaciones clásicas que queremos saber manejar:
- El archivo no existe, pero intentamos leerlo (o al contrario: ya existe y queremos crear en modo "crear solo si no existe").
- El archivo está abierto por otro programa y está bloqueado.
- El usuario no tiene permisos para leer o escribir ese archivo o carpeta.
- La ruta al archivo es incorrecta o contiene caracteres prohibidos.
- La operación se interrumpe inesperadamente (por ejemplo, se acabó el disco).
- Mala práctica: dejar archivos abiertos, no cerrar streams.
Afortunadamente, .NET proporciona todas las herramientas para resolver estos problemas. Son simples — pero, como los cinturones de seguridad, lo importante es no olvidarse de abrocharlos.
2. Principios básicos para trabajar seguro con archivos
Comprueba la existencia del archivo y los permisos por adelantado
Antes de leer un archivo, verifica que exista (por ejemplo, con File.Exists), especialmente si la ruta viene del usuario:
string path = "test.txt";
if (!File.Exists(path))
{
Console.WriteLine("Error: archivo no encontrado.");
return;
}
Antes de escribir, asegúrate de que el directorio existe y de que tienes permisos de escritura (o propaga el error hacia arriba).
Nunca dejes streams abiertos
Usa la palabra clave using para cerrar automáticamente los streams — es la mejor práctica:
using var writer = new StreamWriter("output.txt");
writer.WriteLine("Hello, files!");
// Aquí el archivo ya está cerrado, ¡incluso si ocurrió un error!
Esto protege contra bloqueos "colgados" y fugas de recursos.
Siempre captura las excepciones
Cualquier operación de archivo puede lanzar una excepción. Incluso si el archivo estaba ahí hace un momento — puede ser borrado/movido justo al acceder. Usa la construcción try-catch:
try
{
using var reader = new StreamReader("data.txt");
string line = reader.ReadLine();
Console.WriteLine(line);
}
catch (FileNotFoundException)
{
Console.WriteLine("Archivo no encontrado.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Sin acceso al archivo.");
}
catch (IOException ex)
{
Console.WriteLine($"Error de I/O: {ex.Message}");
}
No confíes en la entrada del usuario
Si la ruta la proporciona el usuario, puede estar mal escrita (o intentar romper tu programa). Es recomendable validar la ruta (ver Path.GetInvalidPathChars()):
try
{
string userPath = Console.ReadLine()!;
if (string.IsNullOrWhiteSpace(userPath))
{
Console.WriteLine("¡La ruta no puede estar vacía!");
return;
}
// Adicional: comprobar caracteres prohibidos
foreach (char c in Path.GetInvalidPathChars())
if (userPath.Contains(c))
{
Console.WriteLine("La ruta contiene caracteres no válidos.");
return;
}
// A partir de aquí trabajar de forma segura con el archivo
}
catch (Exception ex)
{
Console.WriteLine("Error de validación de ruta: " + ex.Message);
}
3. Ejemplo práctico: leemos un archivo "como corresponde"
Subamos un poco el nivel del proyecto de práctica. Imaginemos que necesitamos leer un archivo cuyo nombre introduce el usuario y mostrar su contenido en pantalla. Todo ello con manejo de errores y teniendo en cuenta la codificación.
Console.Write("Introduce la ruta al archivo: ");
string? path = Console.ReadLine();
// Validación de la ruta
if (string.IsNullOrWhiteSpace(path))
{
Console.WriteLine("¡La ruta no puede estar vacía!");
return;
}
foreach (char c in Path.GetInvalidPathChars())
if (path.Contains(c))
{
Console.WriteLine("La ruta contiene caracteres no válidos.");
return;
}
// Intentamos leer el archivo
try
{
if (!File.Exists(path))
{
Console.WriteLine("Archivo no encontrado.");
return;
}
// Indicamos explícitamente la codificación (por ejemplo, UTF-8)
using var reader = new StreamReader(path, Encoding.UTF8);
string content = reader.ReadToEnd();
Console.WriteLine("Contenido del archivo:");
Console.WriteLine(content);
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("No tienes permisos para leer el archivo.");
}
catch (IOException ex)
{
Console.WriteLine("Error de I/O: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Error inesperado: " + ex.Message);
}
Fíjate que este código no se caerá si el archivo desaparece entre la introducción del nombre y la lectura: la excepción será capturada. El stream se cerrará automáticamente al salir del bloque using, incluso si ocurre un error.
4. Archivos para "escritura": evita pérdida de datos
Cuando abrimos un archivo para escribir, especialmente en modo de sobrescritura (false en el segundo parámetro del constructor de StreamWriter), existe el riesgo de borrar datos importantes por accidente. Aquí van algunos consejos:
Comprueba que no estás sobrescribiendo un archivo existente
A veces es útil preguntar al usuario si el archivo ya existe:
if (File.Exists(path))
{
Console.WriteLine("Atención: el archivo ya existe. ¿Sobrescribir? (y/n)");
string answer = Console.ReadLine()!;
if (!answer.Equals("y", StringComparison.OrdinalIgnoreCase))
return;
}
Usa el modo "append" cuando toque
using var writer = new StreamWriter("log.txt", append: true);
writer.WriteLine(DateTime.Now + ": nueva entrada en el log.");
La información antigua no se perderá.
5. Protección contra race conditions y conflictos
A veces varios programas pueden abrir el mismo archivo simultáneamente (por ejemplo, tu cliente C# y Notepad++). Esto puede provocar errores. Por defecto StreamReader y StreamWriter usan modos de acceso compartido basados en FileShare — es decir, permiten que otros lean o no.
Se puede controlar esto explícitamente:
using var stream = new FileStream("data.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream, Encoding.UTF8);
// Lectura...
- FileShare.Read: otros solo pueden leer.
- FileShare.None: sin acceso compartido — el archivo es solo para ti.
Si necesitas que el archivo pueda ser leído y escrito por distintos programas al mismo tiempo, usa el modo correspondiente, pero con cuidado: eso solo es recomendable si entiendes bien las consecuencias.
6. Excepciones inesperadas y qué hacer con ellas
Incluso siguiendo todas las recomendaciones pueden ocurrir fallos, por ejemplo por falta de espacio en disco, una memoria USB dañada o un virus. Aquí algunas excepciones "exóticas" y cómo atraparlas:
- PathTooLongException — la ruta al archivo es demasiado larga (más de 260 caracteres en versiones antiguas de Windows).
- DirectoryNotFoundException — el directorio indicado no se encuentra.
- DriveNotFoundException — por ejemplo, si la ruta es "Z:\\file.txt" y la unidad Z no existe.
- NotSupportedException — por ejemplo, la ruta contiene una combinación no soportada.
Se recomienda tener bloques separados para esas excepciones — al menos registrarlas (log) por separado.
7. Usar archivos temporales para escritura atómica
Problema clásico: escribes un archivo y la aplicación falla a mitad de proceso — obtienes un archivo corrupto. Los programas profesionales suelen usar la estrategia de escritura "atómica":
- Escribir el contenido en un archivo temporal (por ejemplo, "file.txt.tmp").
- Mover el archivo temporal (la operación suele ser atómica a nivel de filesystem) sobre el objetivo (File.Replace).
- El archivo antiguo queda totalmente reemplazado o no cambia — no hay datos medio corruptos.
Ejemplo:
string tempPath = path + ".tmp";
try
{
using var writer = new StreamWriter(tempPath, false, Encoding.UTF8);
// Escribimos todo en el archivo temporal
writer.Write(contentForSave);
// Tras una escritura exitosa reemplazamos el archivo principal
File.Replace(tempPath, path, null); // Mueve el temporal, reemplazando el objetivo (operación atómica)
}
catch (Exception ex)
{
Console.WriteLine("Error al guardar el archivo: " + ex.Message);
// Es recomendable eliminar tempPath si no se necesita
}
En la práctica muchos editores y suites ofimáticas usan esto para garantizar la consistencia de los datos.
8. Usa clases-wrapper para acceso seguro
.NET tiene métodos auxiliares para operaciones "seguras" con archivos. Por ejemplo, File.ReadAllText y File.WriteAllText abren, leen/escriben y cierran el archivo automáticamente. Pero aun así se suelen envolver en try-catch:
try
{
string text = File.ReadAllText("settings.json", Encoding.UTF8);
// Trabajamos con los datos...
}
catch (Exception ex)
{
Console.WriteLine("Error trabajando con el archivo: " + ex.Message);
}
Para archivos grandes usa streams y lee por partes para no comerte toda la memoria.
GO TO FULL VERSION