Python: crea un directorio anidado de forma segura

En este tutorial, repasaremos ejemplos sobre cómo crear de forma segura un directorio anidado en Python 3.5+, así como en versiones anteriores.

Introducción

La manipulación de archivos es una de las habilidades más importantes para dominar en cualquier lenguaje de programación, y hacerlo correctamente es de suma importancia. Cometer un error podría causar un problema en su programa, en otros programas que se ejecutan en el mismo sistema e incluso en el propio sistema.

Pueden ocurrir posibles errores debido a que el directorio principal no existe, o por otros programas que cambian archivos en el sistema de archivos al mismo tiempo, creando algo que se llama una condición de carrera.

Una condición de carrera (en este caso llamada carrera de datos) ocurre cuando dos o más programas quieren crear un archivo con el mismo nombre en el mismo lugar. Si se produce este tipo de error, es muy difícil encontrarlo y corregirlo, ya que no es determinista o, en pocas palabras, pueden ocurrir diferentes cosas según el momento exacto en que los dos corredores compiten por los datos.

En este artículo, veremos cómo crear un subdirectorio en Python de forma segura, paso a paso. A partir de ahora todo funcionará en Mac, Linux y Windows.

Creación segura de un directorio anidado con pathlib

Hay muchas formas de crear un subdirectorio, pero quizás la más simple sea usar el módulo pathlib. El módulo pathlib está hecho principalmente para ayudar a abstraer diferentes sistemas de archivos del sistema operativo y proporcionar una interfaz uniforme para trabajar con la mayoría de ellos.

Gracias a él, su código debería ser independiente de la plataforma. Tenga en cuenta que esto solo funciona en las versiones más nuevas de Python (3.5 y posteriores).

Digamos que tenemos una ruta absoluta de un directorio que se nos da como una cadena y deseamos crear un subdirectorio con un nombre dado. Vamos a crear un directorio llamado OuterDirectory y coloque InnerDirectory dentro de él.

Importaremos Path desde el módulo pathlib, crearemos un objeto Path con la ruta deseada para nuestro nuevo archivo y usaremos el método mkdir() que tiene la siguiente firma:

1
Path.mkdir(mode=0o777, parents=False, exist_ok=False)

El siguiente fragmento de código hace lo que describimos anteriormente:

1
2
3
from pathlib import Path # Import the module
path = Path("/home/kristina/OuterDirectory/InnerDirectory") # Create Path object
path.mkdir() # Cake the directory

Si mkdir() no tiene éxito, no se creará ningún directorio y se generará un error.

mkdir() Opciones y errores

Si ejecuta el código sin crear el OuterDirectory, verá el siguiente error:

1
2
3
4
5
6
Traceback (most recent call last):
  File "makesubdir.py", line 3, in <module>
    path.mkdir()
  File "/home/kristina/anaconda3/lib/python3.7/pathlib.py", line 1230, in mkdir
    self._accessor.mkdir(self, mode)
FileNotFoundError: [Errno 2] No such file or directory: '/home/kristina/OuterDirectory/InnerDirectory'

O si InnerDirectory ya existe:

1
2
3
4
5
6
Traceback (most recent call last):
  File "/home/kristina/Desktop/UNM/makesubdir.py", line 3, in <module>
    path.mkdir()
  File "/home/kristina/anaconda3/lib/python3.7/pathlib.py", line 1230, in mkdir
    self._accessor.mkdir(self, mode)
FileExistsError: [Errno 17] File exists: '/home/kristina/OuterDirectory/InnerDirectory'

Si ya existe un directorio, el error generado será FileExistsError, y si el directorio principal no existe, se generará FileNotFoundError.

Como no queremos que nuestro programa se rompa cada vez que encuentre un error como este, colocaremos este código en un bloque de prueba:

1
2
3
4
5
6
7
8
from pathlib import Path 
path = Path("/home/kristina/OuterDirectory/InnerDir") 
try:
    path.mkdir() 
except OSError:
    print("Failed to make nested directory")
else:
    print("Nested directory made")

Cuando se ejecuta, si el directorio se crea con éxito, la salida será:

1
Nested directory made

Si nos encontramos con errores, se generará lo siguiente:

1
Failed to make a nested directory

El método mkdir() toma tres parámetros: mode, parents y exit_ok.

  • El parámetro mode, si se proporciona, combinado con umask indica qué usuarios tienen privilegios de lectura, escritura y ejecución. De forma predeterminada, todos los usuarios tienen todos los privilegios, lo que podría no ser lo que queremos si la seguridad es un problema. Hablaremos más sobre esto más adelante.
  • parents indica, en caso de que falte el directorio principal, si el método:
    1. Create the missing parent directory itself (true)
    2. Or to raise an error, like in our second example (false)
  • exist_ok especifica si se debe generar FileExistsError si ya existe un directorio con el mismo nombre. Tenga en cuenta que este error seguirá apareciendo si el archivo del mismo nombre no es un directorio.

Asignación de privilegios de acceso

Hagamos un directorio llamado SecondInnerDirectory donde solo el propietario tenga todos los privilegios de lectura, escritura y ejecución, dentro del SecondOuterDirectory inexistente:

1
2
3
from pathlib import Path
path = Path("/home/kristina/SecondOuterDirectory/SecondInnerDirectory")
path.mkdir(mode = 0o007, parents= True, exist_ok= True)

Esto debería ejecutarse sin error. Si navegamos a SecondOuterDirectory y verificamos su contenido desde la consola así:

1
$ ls -al

Deberíamos obtener la salida:

1
2
3
4
total 12
drwxrwxr-x  3 kristina kristina 4096 dec 10 01:26 .
drwxr-xr-x 77 kristina kristina 4096 dec 10 01:26 ..
d------r-x  2 kristina kristina 4096 dec 10 01:26 SecondInnerDirectory

Bien, entonces podemos ver que el directorio principal se creó con éxito, pero los privilegios no son los esperados. El propietario carece de privilegio de escritura.

El problema que tenemos aquí es que umask no nos permite crear los privilegios deseados. Para solucionar esto, guardaremos el valor original de umask, lo cambiaremos temporalmente y, finalmente, lo devolveremos a su valor original usando el método umask() del módulo OS. umask() devuelve el valor antiguo de umask.

Reescribamos nuestro código para probar esto:

1
2
3
4
5
6
7
8
9
from pathlib import Path
import os 

old_mask = os.umask(0) # Saving the old umask value and setting umask to 0

path = Path("/home/kristina/SecondOuterDirectory/SecondInnerDirectory")
path.mkdir(mode = 0o007, parents= True, exist_ok= True)

os.umask(old_mask) # Reverting umask value

Ejecutar este código y usar el comando ls -al nuevamente dará como resultado el siguiente resultado:

1
2
3
4
total 12
drwxrwxrwx  3 kristina kristina 4096 dec 10 01:45 . 
drwxr-xr-x 77 kristina kristina 4096 dec 10 01:45 ..
d------rwx  2 kristina kristina 4096 dec 10 01:45 SecondInnerDirectory

Conclusión

Para manipular archivos de forma segura en muchos sistemas diferentes, necesitamos una forma robusta de manejar errores como carreras de datos. Python ofrece un gran soporte para esto a través del módulo pathlib.

Siempre pueden ocurrir errores cuando se trabaja con sistemas de archivos, y la mejor manera de lidiar con esto es configurar cuidadosamente los sistemas para detectar todos los errores que potencialmente pueden bloquear nuestro programa o causar otros problemas. Escribir código limpio hace que los programas sean duraderos.

Licensed under CC BY-NC-SA 4.0