Gestión de archivos y directorios en Dart

Para interactuar con el sistema de archivos, Dart utiliza las clases File y Directory de la librería dart:io. Es crucial entender que instanciar un objeto File('ruta/al/archivo.txt') no crea el archivo en el disco; simplemente crea un handle (un manejador) en la memoria con esa ruta. El archivo físico solo aparecerá cuando ejecutes una operación de escritura o creación explícita.

Cuando trabajas con I/O (entrada/salida), la decisión más importante es elegir entre métodos síncronos y asíncronos. Los métodos que terminan en Sync (como readAsStringSync()) bloquean el Event Loop, lo que significa que tu aplicación no podrá procesar ninguna otra tarea (como atender una petición HTTP) hasta que el disco responda. En aplicaciones de servidor o CLI de alto rendimiento, debes usar siempre la versión asíncrona que devuelve un Future o un Stream. Por otro lado, para archivos extremadamente grandes, no debes usar readAsBytes() porque intentará cargar todo el contenido en la RAM de golpe; en su lugar, utiliza openRead(), que devuelve un Stream<List<int>> para procesar el archivo por fragmentos (chunks).

Si intentas manipular un archivo en una ruta donde las carpetas intermedias no existen, la operación fallará. Asimismo, si olvidas cerrar un IOSink tras usar openWrite(), podrías dejar descriptores de archivos abiertos, agotando los recursos del sistema operativo.

import 'dart:io';
import 'dart:convert';

void main() async {
  final String pathDir = 'logs_app';
  final String pathFile = '$pathDir/system.log';

  final directory = Directory(pathDir);
  final file = File(pathFile);

  try {
    // 1. Creación de directorios de forma recursiva
    if (!await directory.exists()) {
      await directory.create(recursive: true);
    }

    // 2. Escritura inicial y uso de FileMode
    // FileMode.write sobrescribe si el archivo existe
    await file.writeAsString('--- Inicio de Sesión ---\n', mode: FileMode.write);
    // FileMode.append añade contenido al final
    await file.writeAsString('Evento: Sistema iniciado\n', mode: FileMode.append);

    // 3. Lectura línea por línea (útil para logs)
    print('--- Contenido actual del log ---');
    final lines = await file.readAsLines();
    for (var line in lines) {
      print('LOG: $line');
    }

    // 4. Uso de Streams para escritura incremental (bajo nivel)
    // openWrite devuelve un IOSink para gestionar el flujo de datos
    final sink = await file.openWrite(mode: FileMode.append);
    sink.writeln('Evento: Operación de escritura por stream');
    await sink.flush(); // Asegura que los datos se escriban antes de cerrar
    await sink.close();

    // 5. Metadatos con FileStat
    final stat = await file.stat();
    print('\nMetadatos del archivo:');
    print('Tamaño: ${stat.size} bytes');
    print('Última modificación: ${stat.modified}');
    print('Tipo: ${stat.type}');

    // 6. Listado de archivos en el directorio
    print('\nExplorando directorio $pathDir:');
    await for (final entity in directory.list()) {
      // entity es un FileSystemEntity, usamos type() para saber qué es
      final type = await entity.type;
      print('- ${entity.path} (Tipo: $type)');
    }

    // 7. Limpieza (Borrado)
    // Borramos el archivo y luego el directorio de forma recursiva
    await file.delete();
    await directory.delete(recursive: true);
    print('\nLimpieza completada.');

  } catch (e) {
    print('Error durante la operación de I/O: $e');
  }
}

Análisis del código

En el ejemplo, primero aseguramos la existencia del entorno usando directory.create(recursive: true). El parámetro recursive es fundamental: si intentas crear a/b/c/ y a no existe, fallará a menos que este parámetro sea true.

Cuando usamos file.writeAsString(..., mode: FileMode.append), le indicamos al sistema operativo que no limpie el contenido previo, sino que mueva el puntero de escritura al final del archivo. Esto es mucho más eficiente que leer todo el contenido, concatenar en memoria y volver a escribir.

Para la lectura, file.readAsLines() es una abstracción de alto nivel que facilita el trabajo con textos estructurados. Sin embargo, fíjate en file.openWrite(). Este método nos devuelve un IOSink. A diferencia de writeAsString, que es una operación de “todo o nada”, el IOSink nos permite enviar datos de forma constante, ideal si estuviéramos descargando un archivo o generando un log masivo, ya que permite que el sistema gestione el flujo de bytes sin saturar la memoria.

Al usar directory.list(), obtenemos un Stream<FileSystemEntity>. Esto es una decisión de diseño de dart:io para que, si el directorio contiene un millón de archivos, no se bloquee la memoria intentando cargarlos todos en una List. En su lugar, vamos recibiendo uno por uno conforme el sistema operativo los lee. Finalmente, entity.type nos permite distinguir dinámicamente si lo que estamos iterando es un File o un Directory sin necesidad de comprobaciones manuales de extensiones.

El error frecuente

Un error clásico al trabajar con File es asumir que el constructor crea el archivo. Si intentas hacer esto:

// ERROR: Fallará si la carpeta 'config' no existe
final config = File('config/settings.json');
await config.writeAsString('{"theme": "dark"}');

El método writeAsString fallará con una excepción de “No such file or directory” porque, aunque el objeto File existe en la VM, el sistema de archivos no ha creado la jerarquía de carpetas necesaria. Siempre debes asegurar la existencia del directorio padre con directory.create(recursive: true) antes de intentar escribir en un archivo dentro de él.

76

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio