Inicio > .NET Framework, Apunte, C# > Navegar estructura de directorios

Navegar estructura de directorios


Ahora vamos a hablar de los directorios del sistema de archivos. Siguiendo con la tónica de la entrada relativa a las unidades de disco duro que publiqué hace unos días, vamos a ver cuál es la equivalencia a DriveInfo, pero para directorios.

Sin sorpresas, tenemos que dicho equivalente es la clase DirectoryInfo (ubicada en System.IO, por supuesto). Aunque la estructura de la clase pudiera parecer similar, existen diferencias notables entre ambas. En primer lugar, a DirectoryInfo le pasamos como parámetro del constructor el directorio sobre el cual vamos a trabajar. Es decir:

using System;
using System.IO;
...
DirectoryInfo dir = new DirectoryInfo(@"C:\users\fgomez");
...

A diferencia de DriveInfo, si el directorio que pasamos como argumento no existe, no lanzará una excepción. Al menos no si el directorio no existe. Sin embargo sí pueden ser lanzadas las siguientes excepciones:

  • ArgumentNullException, si el parámetro es nulo. La presencia de esta excepción indica que eres medio güey.
  • ArgumentException, si el parámetro presenta caracteres inválidos para un directorio, como las comillas o los signos de igualdad.
  • SecurityException indica que el usuario que ejecuta la aplicación no tiene permisos para acceder al susodicho directorio.
  • PathTooLongException, si el tamaño de la cadena del nombre excede el default para el sistema, usualmente 248 caracteres.

En mi opinión, solamente el SecurityException es de cuidado y las demás excepciones se podrían evitar simplemente revisando el parámetro que se pasa al constructor. En el caso de una excepción de seguridad es importante siempre atraparla en un bloque try-catch. En los nuevos sistemas operativos Windows Vista y Windows 7 las medidas de seguridad han sido extremadas. Así que no es difícil obtener esta excepción, especialmente cuando se deja al criterio del usuario el directorio a utilizar.

Una cosa que hasta el momento he dejado de largo es que esta clase hereda de otra, abstracta, llamada FileSystemInfo. Esta clase contiene una serie de métodos y propiedades comunes a cualquier archivo de sistema (un directorio es una forma de archivo de sistema, pero también lo es cualquier archivo común, como uno de Word o de texto). De esta forma se garantiza que se puede tratar a un directorio como un tipo de archivo especial. Dicho esto, pasemos a ver las propiedades y métodos más importantes.

La primera propiedad que me gustaría explicar es la de Exists. Esta propiedad devuelve verdadero si el directorio existe físicamente, y falso en caso contrario. Esto es importante, dado que en el constructor podemos pasar como parámetro cualquier ruta de directorio, exista o no exista (siempre que sea válida en su forma, es decir, que no contenga caracteres no válidos en un directorio, que no exceda en tamaño, etc.), por lo que esta propiedad es la primera que debe validarse antes de consultar las otras propiedades. Dado que dichas propiedades (como los atributos del directorio) realizan llamadas al API de Windows para obtener la información del directorio requerido, si éstas se consultan y el directorio no existe recibirás como premio una bonita y brillante excepción. De ahí la importancia de que tengas bien presente esta propiedad.

Por otra parte tenemos la propiedad Name y FullName. La primera te regresa el nombre de la carpeta en la que estás, mientras que la segunda te regresa la ruta completa. En otras palabras, si el directorio es “C:\users\fgomez”, Name te regresará “fgomez” mientras que FullName regresará “C:\users\fgomez”.

Ahora es buen momento para proponer un programilla que vaya ilustrando de lo que vamos hablando. Este programa recibirá como parámetro el nombre de un directorio y mostrará sus propiedades. Con lo que llevamos al momento, éste luciría así:

using System;
using System.IO;
using System.Security;

class Program
{
  static void Main(string[] args)
  {
    string path = null;

    if (args.Length <= 0)
    {
      Console.WriteLine("Ingrese un directorio: ");
      path = Console.ReadLine();
      Console.WriteLine();
    }
    else
    {
      path = args[0];
    }

    try
    {
      DirectoryInfo dir = new DirectoryInfo(path);
      if (dir.Exists)
      {
        Console.WriteLine("Nombre: {0}", dir.Name);
        Console.WriteLine("Nombre completo: {0}", dir.FullName);
      }
      else
      {
        Console.WriteLine("El directorio '{0}' no existe. ",
          dir.FullName);
      }
    }
    catch (ArgumentException ex)
    {
      Console.WriteLine("El directorio '{0}' es inválido. {1}",
        path, ex.Message);
    }
    catch (PathTooLongException ex)
    {
      Console.WriteLine("El directorio '{0}' tiene muchos caracteres. {1}",
        path, ex.Message);
    }
    catch (SecurityException ex)
    {
      Console.WriteLine("No tiene permiso para acceder a '{0}'. {1}",
        path, ex.Message);
    }

    Console.ReadKey(true);
  }
}

El programa es bastante sencillo. Lo primero que hace (líneas 11 – 21) es revisar los argumentos del programa y si no tiene alguno, le pide al usuario que escriba una ruta. Posteriormente, creamos un objeto DirectoryInfo y si éste existe, escribimos su nombre y nombre completo; y si no lo hace, le avisamos al usuario (líneas 24 – 34). Por supuesto, englobamos todo dentro de un try-catch y atrapamos las posibles excepciones que pudieran aparecer (líneas 36 – 51). Este programa lo seguiremos modificando a lo largo del apunte, pero sería bueno que jugaras con él un ratillo, incluso intenta acceder a directorios en los que no tengas permiso de lectura o ingresa un directorio con caracteres inválidos como “C:>\users” para que veas como lanza las excepciones.

Bueno, prosigamos. Algo natural sería poder obtener el directorio padre y el directorio raíz. Esto lo hacemos a través de las propiedades Parent y Root, respectivamente, que nos regresan un objeto DirectoryInfo correspondiente.

Otra propiedad que tenemos son los atributos del directorio, la propiedad Attributes. Ésta regresa una enumeración de tipo FileAttributes que nos da más propiedades sobre el directorio (nótese que se hereda de FileSystemInfo, por lo que esta propiedad también está disponible para cualquier tipo de archivo; obvio tratándose de un directorio varias de los posibles valores no aplicarán). Esta enumeración está marcada con el atributo FlagsAttribute, por lo que se puede realizar operaciones de bits. Como estamos tratando con directorios, siempre tendrá el valor FileAttributes.Directory activado. Por cierto, esta propiedad (así como otras que obtienen información sobre el directorio) lanza un SecurityException (definida en System.Security, por cierto) si uno no tiene permisos de lectura, y un IOException si al momento de invocarse el directorio no está disponible (usualmente por un error de Windows, aunque también puede pasar, por ejemplo, con unidades de red mapeadas como unidades de disco). Y finalmente, nos puede lanzar un DirectoryNotFoundException si el directorio no existe (aunque si validas contra DirectoryInfo.Exists no debería haber problema alguno).

Así, actualizando nuestro código de ejemplo con lo que llevamos, tendríamos el siguiente extracto:

DirectoryInfo dir = new DirectoryInfo(path);
if (dir.Exists)
{
  Console.WriteLine("Nombre: {0}", dir.Name);
  Console.WriteLine("Nombre completo: {0}", dir.FullName);
  Console.WriteLine("Directorio padre: {0}", dir.Parent.Name);
  Console.WriteLine("Directorio raíz: {0}", dir.Root.Name);
  Console.WriteLine("Atributos: {0}", dir.Attributes);
}
else
{
  Console.WriteLine("El directorio '{0}' no existe. ",
    dir.FullName);
}

Hay otras propiedades, heredadas de FileSystemInfo que pueden ser de utilidad. En resumen, para obtener la fecha en la que el directorio fue creado, leído o modificado por última vez, utilizamos las propiedades CreationTime, LastAccessTime, LastWriteTime, respectivamente.

Hasta el momento nuestro programa muestra información sobre el directorio si éste existe, y si no tan sólo muestra un mensaje diciendo que es inexistente. Vamos ahora a modificar el programa para que cuando no exista, cree el directorio en cuestión. Para ello utilizamos el método Create. Con solo invocarlo el directorio se creará. Dicho método puede lanzar un IOException si el directorio no se pudo crear (por ejemplo, que el directorio ya exista de antemano). Una vez que el directorio se crea, llamamos al método Refresh. Éste se encarga de volver a leer los datos del directorio. Esto es importante, ya que la llamada a Create no los refresca en automático y de hecho si después de Create no invocas a Refresh, el valor de Exists seguirá siendo falso. El extracto quedaría de la siguiente forma.

DirectoryInfo dir = new DirectoryInfo(path);
if (!dir.Exists)
{
  Console.WriteLine("El directorio '{0}' no existe. ¿Desea crearlo? (Y/N) ",
    dir.FullName);
  ConsoleKeyInfo key = Console.ReadKey(true);
  if (key.Key == ConsoleKey.Y)
  {
    dir.Create();
    dir.Refresh();
    if (dir.Exists)
      Console.WriteLine("Directorio '{0}' creado. ", dir.FullName);
    else
      Console.WriteLine("No se pudo crear '{0}'. ", dir.FullName);
  }
}

if (dir.Exists)
{
  Console.WriteLine("Nombre: {0}", dir.Name);
  Console.WriteLine("Nombre completo: {0}", dir.FullName);
  Console.WriteLine("Directorio padre: {0}", dir.Parent.Name);
  Console.WriteLine("Directorio raíz: {0}", dir.Root.Name);
  Console.WriteLine("Atributos: {0}", dir.Attributes);
  Console.WriteLine("Creado: {0}", dir.CreationTime);
  Console.WriteLine("Leído: {0}", dir.LastAccessTime);
  Console.WriteLine("Modificado: {0}", dir.LastWriteTime);
}

Modifiqué el orden un poco para que se impriman los valores del directorio recientemente creado.

Aunque no lo pondremos en nuestro programa, te interesará saber que dado un directorio podemos agregarle subdirectorios. Para ello utilizamos el método CreateSubdirectory, que toma como parámetro el nombre del subdirectorio a crear. En esencia lo que hace es crear un nuevo objeto DirectoryInfo pero con la ruta del subdirectorio e invocar al método Create de dicho objeto. Así de simple.

Y ya que hablamos de subdirectorios, es posible obtenerlos invocando al método GetDirectories. Este método tiene dos sobrecargas. La primera no toma parámetro alguno y simplemente devuelve todos los subdirectorios en un array (DirectoryInfo[]). La segunda sobrecarga toma como parámetro un patrón de búsqueda. Por ejemplo, si yo paso como parámetro “A*”, me devolverá en un array todos los subdirectorios que comiencen con A.

De forma análoga, para obtener los archivos que pertenecen al directorio podemos invocar al método GetFiles (sin parámetro para obtener todos los archivos, o pasándole un parámetro con algún patrón de búsqueda). Éste método nos regresa un array de objetos FileInfo (que trataré en otro apunte, pero que también hereda de FileSystemInfo, así que también contamos con la propiedad Name).

Actualizamos nuestro ejemplo.

if (dir.Exists)
{
  Console.WriteLine("Nombre: {0}", dir.Name);
  Console.WriteLine("Nombre completo: {0}", dir.FullName);
  Console.WriteLine("Directorio padre: {0}", dir.Parent.Name);
  Console.WriteLine("Directorio raíz: {0}", dir.Root.Name);
  Console.WriteLine("Atributos: {0}", dir.Attributes);
  Console.WriteLine("Creado: {0}", dir.CreationTime);
  Console.WriteLine("Leído: {0}", dir.LastAccessTime);
  Console.WriteLine("Modificado: {0}", dir.LastWriteTime);

  Console.WriteLine("Subdirectorios:");
  DirectoryInfo[] subdirs = dir.GetDirectories();
  foreach (DirectoryInfo subdir in subdirs)
    Console.WriteLine("-- {0}", subdir.Name);
}

Un par de métodos que quiero comentar pero que no mostraré en el ejemplo. El primero, MoveTo, como su nombre lo indica mueve el directorio actual a una nueva ubicación, la cual se le pasa como parámetro. Se vale que el nombre sea diferente, solo hay que asegurarse que no exista si no quieres que te vomite un IOException.

Y finalmente, el método Delete se encarga de –sorpresa- eliminar el directorio en cuestión. Existen dos sobrecargas de este método. La primera no cuenta con parámetros y simplemente elimina el directorio, pero solo si éste se encuentra vacío y no es el fólder actual de trabajo. De ser así, te lanzará un IOException. Para borrar un directorio que tenga contenido haces una de dos cosas: o eliminas de forma recursiva los archivos y subdirectorios, o bien invocas a la segunda sobrecarga, que toma un valor booleano como parámetro, y le asignas un “true”. Al hacer esto, el método se encargará de eliminar por ti, de forma recursiva, archivos y subdirectorios. Solo asegúrate de que el directorio actual de trabajo no corresponda al directorio en cuestión (es decir, asegúrate que el directorio sea diferente de System.Environment.CurrentDirectory).

Bueno, pues eso es lo básico que necesitas saber sobre directorios. Quiero mencionar un par de cosas antes de dar por finalizado el tema. . Hay algunos métodos que no he mencionado porque quedan fuera del alcance de este apunte. Me refiero a GetAccessControl y SetAccessControl, que se encargan de obtener o establecer directivas y reglas de seguridad sobre un directorio, así como GetFileSystemInfos que devuelve un array con objetos FileSystemInfo que corresponden tanto a subdirectorios como archivos que el directorio en cuestión contiene.

Con respecto al otro punto, me gustaría mencionar que el .NET Framework contiene una clase estática llamada Directory. Ésta cuenta con métodos estáticos que son equivalentes a los que contiene DirectoryInfo, solo que por lo regular toman como primer parámetro la ruta del directorio. En otras palabras, lo que hago con:

DirectoryInfo dir = new DirectoryInfo("C:\\users\\fgomez\\temp");
dir.Create();

lo podría hacer con su equivalente:

Directory.CreateDirectory("C:\\users\\fgomez\\temp");

Las preguntas obvias serían: ¿por qué .NET pone dos formas de hacer lo mismo? ¿No es más cómodo utilizar la clase estática?

La diferencia fundamental consiste en dos cosas: eficiencia y comodidad. En efecto, si voy a estar llamando a varios atributos de un directorio es preferible crear una instancia de DirectoryInfo para que solo haga la revisión de seguridad y de validación del directorio una vez. Las llamadas a los métodos estáticos de Directory harán estas validaciones cada vez que se les invoque, por lo que si vas a trabajar con un directorio de forma seguida, obteniendo sus propiedades, etc., lo mejor será que te crees una instancia de DirectoryInfo y ya. Por otra parte, si tienes un método que haga algún algoritmo sobre archivos y directorios, a través de su clase base FileSystemInfo, es mejor crear la instancia y aprovechar las ventajas del polimorfismo. Fuera de eso, la decisión es meramente personal. En mi caso, se me hace más cómodo el DirectoryInfo para no estar repitiendo la ruta como sería en cada invocación a los métodos estáticos de Directory. Pero esto es meramente subjetivo y al gusto de cada quién.

Ahora sí llegamos al final de este largo apunte. Para finalizar, muestro el código completo del programa ejemplo que hemos trabajado.

using System;
using System.IO;
using System.Security;

class Program
{
  static void Main(string[] args)
  {
    string path = null;

    if (args.Length <= 0)
    {
      Console.WriteLine("Ingrese un directorio: ");
      path = Console.ReadLine();
      Console.WriteLine();
    }
    else
    {
      path = args[0];
    }

    try
    {
      DirectoryInfo dir = new DirectoryInfo(path);
      if (!dir.Exists)
      {
        Console.WriteLine("El directorio '{0}' no existe. ", dir.FullName);
        Console.WrtieLine("¿Desea crearlo? (Y/N) ");
        ConsoleKeyInfo key = Console.ReadKey(true);
        if (key.Key == ConsoleKey.Y)
        {
          dir.Create();
          dir.Refresh();
          if (dir.Exists)
            Console.WriteLine("Directorio '{0}' creado. ", dir.FullName);
          else
            Console.WriteLine("No se pudo crear '{0}'. ", dir.FullName);
        }
      }

      if (dir.Exists)
      {
        Console.WriteLine("Nombre: {0}", dir.Name);
        Console.WriteLine("Nombre completo: {0}", dir.FullName);
        Console.WriteLine("Directorio padre: {0}", dir.Parent.Name);
        Console.WriteLine("Directorio raíz: {0}", dir.Root.Name);
        Console.WriteLine("Atributos: {0}", dir.Attributes);
        Console.WriteLine("Creado: {0}", dir.CreationTime);
        Console.WriteLine("Leído: {0}", dir.LastAccessTime);
        Console.WriteLine("Modificado: {0}", dir.LastWriteTime);

        Console.WriteLine("Contenido:");
        DirectoryInfo[] subdirs = dir.GetDirectories();
        foreach (DirectoryInfo subdir in subdirs)
          Console.WriteLine("[DIR] {0}", subdir.Name);
        FileInfo[] files = dir.GetFiles();
        foreach (FileInfo file in files)
          Console.WriteLine("- {0}", file.Name);
      }
    }
    catch (ArgumentException ex)
    {
      Console.WriteLine("El directorio '{0}' es inválido. {1}",
        path, ex.Message);
    }
    catch (PathTooLongException ex)
    {
      Console.WriteLine("El directorio '{0}' tiene muchos caracteres. {1}",
        path, ex.Message);
    }
    catch (SecurityException ex)
    {
      Console.WriteLine("No tiene permiso para acceder a '{0}'. {1}",
        path, ex.Message);
    }
    catch (DirectoryNotFoundException ex)
    {
      Console.WriteLine("No se pudo encontrar el directorio '{0}'. {1}",
        path, ex.Message);
    }
    catch (IOException ex)
    {
      Console.WriteLine("El directorio '{0}' no está disponible. {1}",
        path, ex.Message);
    }

    Console.ReadKey(true);
  }
}

Al ejecutar este programa contra un directorio existente, obtengo el siguiente resultado:

Ingrese un directorio:
c:\users\fgomez\documents\epeople

Nombre: epeople
Nombre completo: c:\users\fgomez\documents\epeople
Directorio padre: documents
Directorio raíz: c:\
Atributos: Directory
Creado: 01/12/2009 12:42:29 a.m.
Leído: 22/01/2010 02:25:38 p.m.
Modificado: 22/01/2010 02:25:38 p.m.
Contenido:
[DIR] email
[DIR] fondos de escritorio
[DIR] plantillas
- Doc nuevo de UBSA.docx
- glaub an mich.docx

mientras que al ejecutarlo contra uno no existente, el resultado es el siguiente:

Ingrese un directorio:
c:\users\fgomez\temp

El directorio 'c:\users\fgomez\temp' no existe. ¿Desea crearlo? (Y/N)
Directorio 'c:\users\fgomez\temp' creado.
Nombre: temp
Nombre completo: c:\users\fgomez\temp
Directorio padre: fgomez
Directorio raíz: c:\
Atributos: Directory
Creado: 01/04/2010 01:56:34 a.m.
Leído: 01/04/2010 01:56:34 a.m.
Modificado: 01/04/2010 01:56:34 a.m.
Contenido:

Nos vemos en el siguiente apunte, en el que trataremos los archivos junto a la clase FileInfo.

Categorías:.NET Framework, Apunte, C# Etiquetas:
  1. Gonzalo
    abril 22, 2012 a las 1:16 pm

    maestro!!!! Gracias, espectacular

  1. mayo 4, 2010 a las 10:57 pm
  2. septiembre 29, 2010 a las 9:46 am

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s