Compruebe si una ruta es válida en Python sin crear un archivo en el destino de la ruta

Tengo una ruta (incluyendo el directorio y el nombre del archivo).
Necesito probar si el nombre del archivo es válido, por ejemplo, si el sistema de archivos me permitirá crear un archivo con ese nombre.
El nombre del archivo tiene algunos caracteres Unicode en él.

Es seguro asumir que el segmento del directorio de la ruta es válido y accesible ( estaba tratando de hacer que la pregunta fuera más aplicable a nivel mundial, y al parecer estaba demasiado lejos ).

No quiero tener que escapar de nada a menos que tenga que hacerlo.

Publico algunos de los personajes de ejemplo con los que estoy tratando, pero aparentemente son eliminados automáticamente por el sistema de intercambio de stack. De todos modos, quiero mantener entidades Unicode estándar como ö , y solo escapar de las cosas que no son válidas en un nombre de archivo.


Aquí está la trampa. Puede que (o no) ya haya un archivo en el destino de la ruta. Necesito mantener ese archivo si existe, y no crear un archivo si no existe.

Básicamente, quiero comprobar si podría escribir en una ruta sin abrir realmente la ruta para la escritura (y la creación automática de archivos / el locking de archivos que generalmente conlleva).

Como tal:

 try: open(filename, 'w') except OSError: # handle error here 

de aquí

No es aceptable, ya que sobrescribirá el archivo existente, que no quiero tocar (si está allí), o creará dicho archivo si no lo está.

Sé que puedo hacer:

 if not os.access(filePath, os.W_OK): try: open(filePath, 'w').close() os.unlink(filePath) except OSError: # handle error here 

Pero eso creará el archivo en el filePath , que luego tendría que os.unlink .

Al final, parece que está gastando 6 o 7 líneas para hacer algo que debería ser tan simple como os.isvalidpath(filePath) o similar.


Además, necesito esto para ejecutar (al menos) Windows y MacOS, así que me gustaría evitar cosas específicas de la plataforma.

tl; dr

Llame a la función is_path_exists_or_creatable() definida a continuación.

Estrictamente Python 3. Así es como rodamos.

Un cuento de dos preguntas

La pregunta de “¿Cómo pruebo la validez de las rutas y, para las rutas válidas, la existencia o capacidad de escritura de esas rutas?” Es claramente dos preguntas separadas. Ambos son interesantes, y ninguno de los dos ha recibido una respuesta genuinamente satisfactoria aquí … o, bueno, en cualquier lugar al que pudiera ir.

La respuesta de vikki probablemente es la más cercana, pero tiene las notables desventajas de:

  • Sin necesidad de abrir ( … y luego no cerrar de manera confiable ) los manejadores de archivos.
  • No es necesario escribir ( … y luego fallar al cerrar o eliminar de manera confiable ) los archivos de 0 bytes.
  • Ignorar los errores específicos del sistema operativo que diferencian las rutas de acceso no válidas y los problemas del sistema de archivos que no se pueden ignorar. Como era de esperar, esto es crítico en Windows. ( Ver abajo ) .
  • Ignorar las condiciones de carrera que resultan de procesos externos al mismo tiempo (re) mover directorios primarios de la ruta a probar. ( Ver abajo ) .
  • Ignorar los tiempos de espera de conexión resultantes de esta ruta de acceso que reside en sistemas de archivos obsoletos, lentos o inaccesibles temporalmente. Esto podría exponer a los servicios públicos a posibles ataques impulsados ​​por DoS . ( Ver abajo ) .

Vamos a arreglar todo eso.

Pregunta # 0: ¿Cuál es la validez del nombre de ruta otra vez?

Antes de lanzar nuestros frágiles trajes de carne a los moshpits de dolor plagados de pitones, probablemente deberíamos definir qué entendemos por “validez de nombre de ruta”. ¿Qué define la validez, exactamente?

Por “validez de ruta de acceso”, nos referimos a la corrección sintáctica de una ruta de acceso con respecto al sistema de archivos raíz del sistema actual, independientemente de si esa ruta o sus directorios principales existen físicamente. Una ruta de acceso es sintácticamente correcta 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 se monta en el directorio raíz ( / ).
  • En Windows, el sistema de archivos se montó en %HOMEDRIVE% , la letra de la unidad con el sufijo de dos puntos que contiene la instalación actual de Windows (normalmente, pero no necesariamente, C: %HOMEDRIVE% .

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 la mayoría, pero no todos los compatibles con POSIX), una ruta es sintácticamente correcta si y solo si esa ruta:

  • No contiene bytes nulos (es decir, \x00 en Python). Este es un requisito difícil 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 ruta es una subcadena más larga de una ruta que contiene no / carácter (por ejemplo, bergtatt , ind , i y fjeldkamrene en la ruta de acceso /bergtatt/ind/i/fjeldkamrene ).

Corrección sintáctica. Sistema de archivos raíz. Eso es.

Pregunta # 1: ¿Cómo hacemos ahora la validez del nombre de ruta?

Validar las rutas de acceso en Python es sorprendentemente no intuitivo. Estoy en firme acuerdo con Nombre falso aquí: el paquete oficial os.path debería proporcionar una solución lista para usar para esto. Por razones desconocidas (y probablemente no convincentes), no lo hace. Afortunadamente, desenrollar su propia solución ad-hoc no es tan desgarrador …

OK, en realidad lo es. Es peludo es desagradable; Probablemente se ríe mientras gruñe y se ríe a medida que brilla. Pero que vas a hacer? Nuthin ‘.

Pronto descenderemos al abismo radioactivo del código de bajo nivel. Pero primero, hablemos de la tienda de alto nivel. Las os.stat() estándar os.stat() y os.lstat() las siguientes excepciones cuando se pasan rutas de acceso no válidas:

  • Para las rutas que residen en directorios no existentes, instancias de FileNotFoundError .
  • Para las rutas que residen en directorios existentes:
    • En Windows, instancias de WindowsError cuyo atributo winerror es 123 (es decir, ERROR_INVALID_NAME ).
    • Bajo todos los otros sistemas operativos:
    • Para las rutas que contienen bytes nulos (es decir, '\x00' ), instancias de TypeError .
    • Para las rutas que contienen componentes de ruta de más de 255 bytes, instancias de OSError cuyo atributo errcode es:
      • Bajo SunOS y la familia de sistemas operativos * BSD, errno.ERANGE . (Esto parece ser un error a nivel de sistema operativo, también denominado “interpretación selectiva” del estándar POSIX).
      • Bajo todos los demás sistemas operativos, errno.ENAMETOOLONG .

De manera crucial, esto implica que solo las rutas de acceso que residen en directorios existentes son validables. Las os.stat() y os.lstat() FileNotFoundError excepciones genéricas de FileNotFoundError cuando se pasan las FileNotFoundError que residen en directorios no existentes, independientemente de si esas rutas son inválidas o no. La existencia del directorio tiene prioridad sobre la invalidez del nombre de ruta.

¿Significa esto que las rutas 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, ¿eso es incluso factible? ¿La modificación de una ruta de acceso no debería impedirnos validar la ruta de acceso original?

Para responder a esta pregunta, recuerde desde arriba que las rutas de acceso sintácticamente correctas en el sistema de archivos ext4 no contienen componentes de ruta (A) que contengan bytes nulos o (B) de más de 255 bytes de longitud. Por lo tanto, una ruta de acceso ext4 es válida solo si todos los componentes de ruta en esa ruta de acceso son válidos. Esto es cierto para la mayoría de los sistemas de archivos del mundo real de interés.

¿Nos ayuda esa visión pedante? Sí. Reduce el problema mayor de validar la ruta completa de una sola vez al problema más pequeño de validar solo todos los componentes de ruta en esa ruta. Cualquier ruta de acceso arbitraria es válida (independientemente de si esa ruta de acceso reside en un directorio existente o no) de forma multiplataforma siguiendo el siguiente algoritmo:

  1. Divida ese nombre de ruta en componentes de ruta (por ejemplo, nombre de ruta /troldskog/faren/vild en la lista ['', 'troldskog', 'faren', 'vild'] ).
  2. Para cada uno de estos componentes:
    1. Únase a la ruta de acceso de un directorio que se garantiza que existe con ese componente en una nueva ruta de acceso temporal (por ejemplo, /troldskog ).
    2. 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 FileNotFoundError excepción que FileNotFoundError el tipo de invalidez en lugar de una excepción genérica de FileNotFoundError . ¿Por qué? Porque esa ruta reside en un directorio existente. (La lógica circular es circular.)

¿Existe un directorio que se garantice que exista? Sí, pero generalmente solo uno: el directorio superior del sistema de archivos raíz (como se definió anteriormente).

Al pasar las rutas de acceso que residen en cualquier otro directorio (y, por lo tanto, no se garantiza que existan) a os.stat() o os.lstat() invitan las condiciones de carrera, incluso si ese directorio se probó previamente para existir. ¿Por qué? Debido a que no se puede impedir que los procesos externos os.stat() ese directorio una vez que se haya realizado esa prueba, pero antes de que la ruta se pase a os.stat() o os.lstat() . ¡Desata a los perros de la locura de los que se quejan de la mente!

También existe un beneficio colateral sustancial para el enfoque anterior: la seguridad. (¿No es eso bueno?) Específicamente:

Las aplicaciones de frente que validan rutas de acceso arbitrarias de fonts no confiables simplemente pasándolas a os.stat() o os.lstat() son susceptibles a ataques de Denegación de Servicio (DoS) y otros chanchullos de sombrero negro. Los usuarios malintencionados pueden intentar validar repetidamente las rutas que residen en sistemas de archivos que se sabe que son obsoletos o lentos (p. Ej., Recursos compartidos Samba de NFS); en ese caso, establecer ciegamente las rutas de acceso entrantes puede fallar con el tiempo de espera de la conexión o consumir más tiempo y recursos que su débil capacidad para soportar el desempleo.

El enfoque anterior evita esto al solo validar los componentes de la ruta de acceso de una ruta contra 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).

¿Perdido? Genial. Vamos a empezar. (Python 3 asumió. Consulte “¿Qué es la esperanza frágil para 300, leycec ?”)

 import errno, os # Sadly, Python fails to provide the following magic number for us. ERROR_INVALID_NAME = 123 ''' Windows-specific error code indicating an invalid pathname. See Also ---------- https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx 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. ''' # If this pathname is either not a string or is but is empty, this pathname # is invalid. try: if not isinstance(pathname, str) or not pathname: return False # Strip this pathname's Windows-specific drive specifier (eg, `C:\`) # if any. Since Windows prohibits path components from containing `:` # characters, failing to strip this `:`-suffixed prefix would # erroneously invalidate all valid absolute Windows pathnames. _, pathname = os.path.splitdrive(pathname) # Directory guaranteed to exist. If the current OS is Windows, this is # the drive to which Windows was installed (eg, the "%HOMEDRIVE%" # environment variable); else, the typical root directory. root_dirname = os.environ.get('HOMEDRIVE', 'C:') \ if sys.platform == 'win32' else os.path.sep assert os.path.isdir(root_dirname) # ...Murphy and her ironclad Law # Append a path separator to this directory if needed. root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep # Test whether each path component split from this pathname is valid or # not, ignoring non-existent and non-readable path components. for pathname_part in pathname.split(os.path.sep): try: os.lstat(root_dirname + pathname_part) # If an OS-specific exception is raised, its error code # indicates whether this pathname is valid or not. Unless this # is the case, this exception implies an ignorable kernel or # filesystem complaint (eg, path not found or inaccessible). # # Only the following exceptions indicate invalid pathnames: # # * Instances of the Windows-specific "WindowsError" class # defining the "winerror" attribute whose value is # "ERROR_INVALID_NAME". Under Windows, "winerror" is more # fine-grained and hence useful than the generic "errno" # attribute. When a too-long pathname is passed, for example, # "errno" is "ENOENT" (ie, no such file or directory) rather # than "ENAMETOOLONG" (ie, file name too long). # * Instances of the cross-platform "OSError" class defining the # generic "errno" attribute whose value is either: # * Under most POSIX-compatible OSes, "ENAMETOOLONG". # * Under some edge-case OSes (eg, SunOS, *BSD), "ERANGE". 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 # If a "TypeError" exception was raised, it almost certainly has the # error message "embedded NUL character" indicating an invalid pathname. except TypeError as exc: return False # If no exception was raised, all path components and hence this # pathname itself are valid. (Praise be to the curmudgeonly python.) else: return True # If any other exception was raised, this is an unrelated fatal issue # (eg, a bug). Permit this exception to unwind the call stack. # # Did we mention this should be shipped with Python already? 

Hecho. No entrecierres los ojos en ese código. ( Muerde. )

Pregunta n. ° 2: Posiblemente no sea válida la existencia del nombre de ruta o la creatividad, ¿eh?

La comprobación de la existencia o la capacidad de creación de rutas de acceso posiblemente no válidas 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. ''' # Parent directory of the passed path. If empty, we substitute the current # working directory (CWD) instead. 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: # To prevent "os" module calls from raising undesirable exceptions on # invalid pathnames, is_pathname_valid() is explicitly called first. return is_pathname_valid(pathname) and ( os.path.exists(pathname) or is_path_creatable(pathname)) # Report failure on non-fatal filesystem complaints (eg, connection # timeouts, permissions issues) implying this path to be inaccessible. All # other exceptions are unrelated fatal issues and should not be caught here. except OSError: return False 

Hecho y hecho. Excepto no del todo.

Pregunta n. ° 3: Posibilidad o capacidad de escritura de nombre de ruta posiblemente no válidas en Windows

Existe una advertencia. Por supuesto que sí.

Como admite la documentación oficial de os.access() :

Nota: Las operaciones de E / S pueden fallar incluso cuando os.access() indica que tendrían éxito, particularmente para operaciones en sistemas de archivos de red que pueden tener semánticas de permisos más allá del modelo de bit de permiso POSIX habitual.

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 de bit de permiso POSIX simplista se correlaciona mal con la realidad subyacente de Windows. Si bien esto (posiblemente) no es culpa de Python, podría ser un problema para las aplicaciones compatibles con Windows.

Si este eres tú, se busca una alternativa más robusta. Si la ruta pasada no existe, en su lugar, intentamos crear un archivo temporal que se garantice que se elimine de inmediato en el directorio principal de esa ruta, una prueba más portátil (aunque costosa) de la capacidad de creación:

 import os, tempfile def is_path_sibling_creatable(pathname: str) -> bool: ''' `True` if the current user has sufficient permissions to create **siblings** (ie, arbitrary files in the parent directory) of the passed pathname; `False` otherwise. ''' # Parent directory of the passed path. If empty, we substitute the current # working directory (CWD) instead. dirname = os.path.dirname(pathname) or os.getcwd() try: # For safety, explicitly close and hence delete this temporary file # immediately after creating it in the passed path's parent directory. with tempfile.TemporaryFile(dir=dirname): pass return True # While the exact type of exception raised by the above function depends on # the current version of the Python interpreter, all such types subclass the # following exception superclass. 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: # To prevent "os" module calls from raising undesirable exceptions on # invalid pathnames, is_pathname_valid() is explicitly called first. return is_pathname_valid(pathname) and ( os.path.exists(pathname) or is_path_sibling_creatable(pathname)) # Report failure on non-fatal filesystem complaints (eg, connection # timeouts, permissions issues) implying this path to be inaccessible. All # other exceptions are unrelated fatal issues and should not be caught here. except OSError: return False 

Tenga en cuenta, sin embargo, que incluso esto puede no ser suficiente.

Gracias al Control de acceso de usuarios (UAC), el inimitable Windows Vista y todas las iteraciones subsiguientes 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 C:\Windows\system32 , UAC permite al usuario hacerlo de manera superficial, mientras que 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.

Ya que NULL es el único carácter prohibido en las rutas de los sistemas de archivos orientados a UNIX, aprovechemos eso para demostrar la verdad fría y dura, ignorando los chanchullos de Windows que no se pueden ignorar, 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á problemas de portabilidad de Python.

 if os.path.exists(filePath): #the file is there elif os.access(os.path.dirname(filePath), os.W_OK): #the file does not exists but write privileges are given else: #can not write there 

Tenga en cuenta que path.exists puede fallar por más razones por las que the file is not there por lo que es posible que tenga que realizar pruebas más precisas, como comprobar si existe el directorio que lo contiene, etc.


Después de mi discusión con el OP, resultó que el problema principal parece ser que el nombre del archivo podría contener caracteres que no están permitidos por el sistema de archivos. Por supuesto, deben eliminarse, pero el OP desea mantener la legibilidad humana tanto como lo permita el sistema de archivos.

Lamentablemente no conozco ninguna buena solución para esto. Sin embargo, la respuesta de Cecil Curry analiza más detenidamente la detección del problema.

Con Python 3, ¿qué tal?

 try: with open(filename, 'x') as tempfile: # OSError if file exists or is invalid pass except OSError: # handle error here 

Con la opción ‘x’ tampoco tenemos que preocuparnos por las condiciones de la carrera. Vea la documentación aquí .

Ahora, esto CREARÁ un archivo temporal muy breve si aún no existe, a menos que el nombre no sea válido. Si puedes vivir con eso, simplifica mucho las cosas.

 open(filename,'r') #2nd argument is r and not w 

abrirá el archivo o dará un error si no existe. Si hay un error, entonces puedes intentar escribir en la ruta, si no puedes, entonces obtienes un segundo error

 try: open(filename,'r') return True except IOError: try: open(filename, 'w') return True except IOError: return False 

También eche un vistazo aquí sobre los permisos en Windows

intente os.path.exists esto comprobará la ruta y devolverá True si existe y False si no existe.