tl; dr
Llame a la is_path_exists_or_creatable()
función definida a continuación.
Estrictamente Python 3. Así es como rodamos.
Historia de dos preguntas
La pregunta de "¿Cómo pruebo la validez del nombre de ruta y, para nombres de ruta válidos, la existencia o capacidad de escritura de esas rutas?" son claramente dos preguntas separadas. Ambos son interesantes, y ninguno ha recibido una respuesta genuinamente satisfactoria aquí ... o, bueno, en cualquier lugar donde pudiera grep.
La respuesta de vikki probablemente sea la más cercana, pero tiene las notables desventajas de:
- Abrir innecesariamente ( ... y luego no cerrar de manera confiable ) identificadores de archivos.
- Escribir innecesariamente ( ... y luego fallar al cerrar o eliminar de manera confiable ) archivos de 0 bytes.
- Ignorar errores específicos del sistema operativo que diferencian entre rutas de acceso inválidas no ignorables y problemas del sistema de archivos ignorables. Como era de esperar, esto es fundamental en Windows. ( Ver más abajo ) .
- Ignorar las condiciones de carrera que resultan de procesos externos al mismo tiempo (re) mover directorios principales del nombre de ruta que se va a probar. ( Ver más abajo ) .
- Ignorar los tiempos de espera de conexión que resultan de este nombre de ruta que reside en sistemas de archivos obsoletos, lentos o temporalmente inaccesibles. Esto podría exponer los servicios públicos a posibles ataques impulsados por DoS . ( Ver más abajo ) .
Vamos a arreglar todo eso.
Pregunta # 0: ¿Qué es la validez del nombre de ruta nuevamente?
Antes de arrojar nuestros frágiles trajes de carne a los moshpits de dolor plagados de pitones, probablemente deberíamos definir lo que queremos decir con "validez de nombre de ruta". ¿Qué define la validez exactamente?
Por "validez de nombre de ruta", nos referimos a la corrección sintáctica de un nombre de ruta con respecto a la sistema de archivos raíz del sistema actual, independientemente de si esa ruta o los directorios principales del mismo existen físicamente. Un nombre de ruta es sintácticamente correcto según esta definición si cumple con todos los requisitos sintácticos del sistema de archivos raíz.
Por "sistema de archivos raíz", queremos decir:
- En sistemas compatibles con POSIX, el sistema de archivos montado en el directorio raíz (
/
).
- En Windows, el sistema de archivos se monta en
%HOMEDRIVE%
la letra de unidad con el sufijo de dos puntos que contiene la instalación actual de Windows (normalmente, pero no necesariamente C:
).
El significado de "corrección sintáctica", a su vez, depende del tipo de sistema de archivos raíz. Para los sistemas de archivos ext4
(y para la mayoría, pero no todos, compatibles con POSIX), un nombre de ruta es sintácticamente correcto si y solo si ese nombre de ruta:
- No contiene bytes nulos (es decir,
\x00
en Python). Este es un requisito estricto para todos los sistemas de archivos compatibles con POSIX.
- No contiene componentes de ruta de más de 255 bytes (por ejemplo,
'a'*256
en Python). Un componente de la ruta es una subcadena más larga de una ruta de acceso que no contiene /
caracteres (por ejemplo, bergtatt
, ind
, i
, y fjeldkamrene
en el nombre de ruta /bergtatt/ind/i/fjeldkamrene
).
Corrección sintáctica. Sistema de archivos raíz. Eso es.
Pregunta n. ° 1: ¿Cómo haremos ahora la validez del nombre de ruta?
Validar nombres de rutas en Python es sorprendentemente poco intuitivo. Estoy completamente de acuerdo con Fake Name aquí: el os.path
paquete oficial debería proporcionar una solución lista para usar para esto. Por razones desconocidas (y probablemente poco convincentes), no es así. Afortunadamente, desenrollando su propia solución ad-hoc no es que desgarrador ...
OK, en realidad lo es. Es peludo; es desagradable; probablemente se ríe mientras burbujea y se ríe mientras brilla. Pero que vas a hacer Nada.
Pronto descenderemos al abismo radiactivo del código de bajo nivel. Pero primero, hablemos de la tienda de alto nivel. El estándar os.stat()
y las os.lstat()
funciones generan las siguientes excepciones cuando se pasan nombres de ruta no válidos:
- Para los nombres de ruta que residen en directorios no existentes, instancias de
FileNotFoundError
.
- Para rutas que residen en directorios existentes:
- En Windows, instancias de
WindowsError
cuyo winerror
atributo es 123
( es decir, ERROR_INVALID_NAME
).
- En todos los demás sistemas operativos:
- Para rutas que contienen bytes nulos (es decir,
'\x00'
), instancias de TypeError
.
- Para nombres de ruta que contienen componentes de ruta de más de 255 bytes, instancias de
OSError
cuyo errcode
atributo es:
- En SunOS y la familia * BSD de sistemas operativos,
errno.ERANGE
. (Esto parece ser un error a nivel del sistema operativo, también conocido como "interpretación selectiva" del estándar POSIX).
- En todas las demás sistemas operativos,
errno.ENAMETOOLONG
.
Fundamentalmente, esto implica que solo los nombres de ruta que residen en directorios existentes son validables. Las funciones os.stat()
y os.lstat()
generan FileNotFoundError
excepciones genéricas cuando se pasan nombres de ruta que residen en directorios no existentes, independientemente de si esos nombres de ruta no son válidos o no. La existencia del directorio tiene prioridad sobre la invalidez del nombre de ruta.
¿Significa esto que los nombres de ruta que residen en directorios no existentes no son validables? Sí, a menos que modifiquemos esos nombres de ruta para que residan en directorios existentes. Sin embargo, ¿es eso factible incluso con seguridad? ¿La modificación de un nombre de ruta no debería impedirnos validar el nombre de ruta original?
Para responder a esta pregunta, recuerde que los nombres de ruta sintácticamente correctos en el ext4
sistema de archivos no contienen componentes de ruta (A) que contengan bytes nulos o (B) de más de 255 bytes de longitud. Por lo tanto, un ext4
nombre de ruta es válido si y solo si todos los componentes de la ruta en ese nombre de ruta son válidos. Esto es cierto para la mayoría de los sistemas de archivos de interés del mundo real .
¿Nos ayuda realmente esa percepción pedante? Si. Reduce el problema mayor de validar el nombre de ruta completo de una sola vez al problema más pequeño de solo validar todos los componentes de la ruta en ese nombre de ruta. Cualquier nombre de ruta arbitrario es validable (independientemente de si ese nombre de ruta reside en un directorio existente o no) de una manera multiplataforma siguiendo el siguiente algoritmo:
- Divida ese nombre de ruta en componentes de ruta (por ejemplo, el nombre de ruta
/troldskog/faren/vild
en la lista ['', 'troldskog', 'faren', 'vild']
).
- Para cada uno de estos componentes:
- Une el nombre de ruta de un directorio que se garantiza que existe con ese componente en un nuevo nombre de ruta temporal (por ejemplo,
/troldskog
).
- Pase ese nombre de ruta a
os.stat()
o os.lstat()
. Si ese nombre de ruta y, por lo tanto, ese componente no es válido, se garantiza que esta llamada generará una excepción que exponga el tipo de invalidez en lugar de una FileNotFoundError
excepción genérica . ¿Por qué? Porque ese nombre de ruta reside en un directorio existente. (La lógica circular es circular).
¿Existe un directorio garantizado? Sí, pero normalmente solo uno: el directorio superior del sistema de archivos raíz (como se definió anteriormente).
Pasar nombres de ruta que residen en cualquier otro directorio (y por lo tanto no se garantiza que exista) os.stat()
o os.lstat()
invita a condiciones de carrera, incluso si ese directorio se probó previamente para su existencia. ¿Por qué? Debido a que no se puede evitar que los procesos externos eliminen simultáneamente ese directorio después de que se haya realizado esa prueba, pero antes de que se pase ese nombre de ruta a os.stat()
o os.lstat()
. ¡Libera a los perros de la locura que devora la mente!
También existe un beneficio secundario sustancial del enfoque anterior: la seguridad. (No se tiene que agradable?) Específicamente:
Aplicaciones frontales que validan nombres de rutas arbitrarios de fuentes no confiables simplemente pasando dichos nombres de ruta os.stat()
o os.lstat()
son susceptibles a ataques de denegación de servicio (DoS) y otras travesuras de sombrero negro. Los usuarios malintencionados pueden intentar validar repetidamente los nombres de ruta que residen en sistemas de archivos que se sabe que están obsoletos o lentos (por ejemplo, recursos compartidos de NFS Samba); en ese caso, establecer ciegamente los nombres de las rutas entrantes puede eventualmente fallar con tiempos de espera de conexión o consumir más tiempo y recursos que su débil capacidad para soportar el desempleo.
El enfoque anterior evita esto validando únicamente los componentes de la ruta de un nombre de ruta con el directorio raíz del sistema de archivos raíz. (Si incluso eso es obsoleto, lento o inaccesible, tiene problemas más grandes que la validación del nombre de ruta).
¿Perdió? Excelente. Vamos a empezar. (Se asume Python 3. Consulte "¿Qué es la esperanza frágil para 300, leycec ?")
import errno, os
ERROR_INVALID_NAME = 123
'''
Windows-specific error code indicating an invalid pathname.
See Also
----------
https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
Official listing of all such codes.
'''
def is_pathname_valid(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname for the current OS;
`False` otherwise.
'''
try:
if not isinstance(pathname, str) or not pathname:
return False
_, pathname = os.path.splitdrive(pathname)
root_dirname = os.environ.get('HOMEDRIVE', 'C:') \
if sys.platform == 'win32' else os.path.sep
assert os.path.isdir(root_dirname)
root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep
for pathname_part in pathname.split(os.path.sep):
try:
os.lstat(root_dirname + pathname_part)
except OSError as exc:
if hasattr(exc, 'winerror'):
if exc.winerror == ERROR_INVALID_NAME:
return False
elif exc.errno in {errno.ENAMETOOLONG, errno.ERANGE}:
return False
except TypeError as exc:
return False
else:
return True
Hecho. No entrecerre los ojos ante ese código. ( Muerde. )
Pregunta # 2: ¿Posiblemente existencia o capacidad de creación de nombre de ruta no válido, eh?
Probar la existencia o capacidad de creación de nombres de ruta posiblemente inválidos es, dada la solución anterior, en su mayoría trivial. La pequeña clave aquí es llamar a la función previamente definida antes de probar la ruta pasada:
def is_path_creatable(pathname: str) -> bool:
'''
`True` if the current user has sufficient permissions to create the passed
pathname; `False` otherwise.
'''
dirname = os.path.dirname(pathname) or os.getcwd()
return os.access(dirname, os.W_OK)
def is_path_exists_or_creatable(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname for the current OS _and_
either currently exists or is hypothetically creatable; `False` otherwise.
This function is guaranteed to _never_ raise exceptions.
'''
try:
return is_pathname_valid(pathname) and (
os.path.exists(pathname) or is_path_creatable(pathname))
except OSError:
return False
Hecho y hecho. Excepto que no del todo.
Pregunta # 3: Posiblemente existencia de nombre de ruta no válido o capacidad de escritura en Windows
Existe una advertencia. Por supuesto que sí.
Como admite la os.access()
documentación oficial :
Nota: Las operaciones de E / S pueden fallar incluso cuando os.access()
indique que tendrán éxito, particularmente para operaciones en sistemas de archivos de red que pueden tener una semántica de permisos más allá del modelo habitual de bits de permiso POSIX.
Para sorpresa de nadie, Windows es el sospechoso habitual aquí. Gracias al uso extensivo de las listas de control de acceso (ACL) en los sistemas de archivos NTFS, el modelo simplista de bits de permiso POSIX se asigna mal a la realidad subyacente de Windows. Si bien esto (posiblemente) no es culpa de Python, podría ser una preocupación para las aplicaciones compatibles con Windows.
Si este es usted, se busca una alternativa más robusta. Si la ruta pasada no existe, en su lugar, intentamos crear un archivo temporal garantizado para ser eliminado inmediatamente en el directorio principal de esa ruta, una prueba de capacidad de creación más portátil (aunque costosa):
import os, tempfile
def is_path_sibling_creatable(pathname: str) -> bool:
'''
`True` if the current user has sufficient permissions to create **siblings**
(i.e., arbitrary files in the parent directory) of the passed pathname;
`False` otherwise.
'''
dirname = os.path.dirname(pathname) or os.getcwd()
try:
with tempfile.TemporaryFile(dir=dirname): pass
return True
except EnvironmentError:
return False
def is_path_exists_or_creatable_portable(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname on the current OS _and_
either currently exists or is hypothetically creatable in a cross-platform
manner optimized for POSIX-unfriendly filesystems; `False` otherwise.
This function is guaranteed to _never_ raise exceptions.
'''
try:
return is_pathname_valid(pathname) and (
os.path.exists(pathname) or is_path_sibling_creatable(pathname))
except OSError:
return False
Sin embargo, tenga en cuenta que incluso esto puede no ser suficiente.
Gracias al Control de acceso de usuario (UAC), el siempre inimitable Windows Vista y todas las iteraciones posteriores del mismo mienten descaradamente sobre los permisos que pertenecen a los directorios del sistema. Cuando los usuarios que no son administradores intentan crear archivos en los directorios canónicos C:\Windows
o en los C:\Windows\system32
directorios, UAC permite superficialmente al usuario hacerlo mientras en realidad aísla todos los archivos creados en una "Tienda virtual" en el perfil de ese usuario. (¿Quién podría haber imaginado que engañar a los usuarios tendría consecuencias perjudiciales a largo plazo?)
Esto es Loco. Esto es Windows.
Pruébalo
¿Nos atrevemos? Es hora de probar las pruebas anteriores.
Dado que NULL es el único carácter prohibido en los nombres de ruta en los sistemas de archivos orientados a UNIX, aprovechemos eso para demostrar la cruda y dura verdad, ignorando las travesuras de Windows que no son ignorables, que francamente me aburren y me enojan en igual medida:
>>> print('"foo.bar" valid? ' + str(is_pathname_valid('foo.bar')))
"foo.bar" valid? True
>>> print('Null byte valid? ' + str(is_pathname_valid('\x00')))
Null byte valid? False
>>> print('Long path valid? ' + str(is_pathname_valid('a' * 256)))
Long path valid? False
>>> print('"/dev" exists or creatable? ' + str(is_path_exists_or_creatable('/dev')))
"/dev" exists or creatable? True
>>> print('"/dev/foo.bar" exists or creatable? ' + str(is_path_exists_or_creatable('/dev/foo.bar')))
"/dev/foo.bar" exists or creatable? False
>>> print('Null byte exists or creatable? ' + str(is_path_exists_or_creatable('\x00')))
Null byte exists or creatable? False
Más allá de la cordura. Más allá del dolor. Encontrará preocupaciones sobre la portabilidad de Python.