web-dev-qa-db-fra.com

Vérifier si un chemin est valide dans Python sans créer de fichier à la cible du chemin

J'ai un chemin (y compris le nom du répertoire et du fichier).
Je dois tester si le nom du fichier est valide, par exemple. si le système de fichiers me permettra de créer un fichier avec un tel nom.
Le nom du fichier contient des caractères unicode.

Il est prudent de supposer que le segment de répertoire du chemin est valide et accessible (J'essayais de rendre la question plus généralement applicable, et apparemment j'étais trop loin).

Je ne veux vraiment pas avoir à échapper à quoi que ce soit sauf si je ai.

Je publierais quelques exemples de personnages avec lesquels je traite, mais apparemment, ils sont automatiquement supprimés par le système d'échange de piles. Quoi qu'il en soit, je souhaite conserver les entités Unicode standard telles que ö Et n'échapper que les éléments non valides dans un nom de fichier.


Voici la prise. Il peut (ou ne peut pas) déjà être un fichier à la cible du chemin. Je dois conserver ce fichier s'il existe, et ne pas créer un fichier si ce n'est pas le cas.

Fondamentalement, je veux vérifier si pourrait écrire sur un chemin sans ouvrir réellement le chemin pour l'écriture (et le création de fichiers/agglutination de fichiers qui implique généralement).

En tant que tel:

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

à partir d'ici

N’est pas acceptable, car cela écrasera le fichier existant, ce que je ne veux pas toucher (s’il existe), ou créera ledit fichier s’il ne l’est pas.

Je sais que je peux faire:

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

Mais cela va créer le fichier au filePath, ce qui me faudrait alors os.unlink.

En fin de compte, il semble qu'il faille dépenser 6 ou 7 lignes pour faire quelque chose qui devrait être aussi simple que os.isvalidpath(filePath) ou similaire.


En passant, j'ai besoin de ça pour (au moins) Windows et MacOS, donc j'aimerais éviter les trucs spécifiques à la plate-forme.

``

68
Fake Name

tl; dr

Appelez la fonction is_path_exists_or_creatable() définie ci-dessous.

Strictly Python 3. C'est comme ça que nous roulons.

Un conte de deux questions

La question "Comment puis-je tester la validité du nom de chemin et, pour les noms de chemin valides, l'existence ou l'écriture de ces chemins?" est clairement deux questions distinctes. Les deux sont intéressants, et ni ont reçu une réponse véritablement satisfaisante ici ... ou bien, n'importe où que je pourrais grep.

vikki 's answer coupe probablement le plus proche, mais présente les inconvénients notables de:

  • Ouvrir inutilement (... et ne pas fermer de manière fiable) gère le fichier.
  • Écriture inutile (..., puis échec de la fermeture ou de la suppression fiable) fichiers de 0 octet.
  • Ignorer les erreurs spécifiques au système d'exploitation faisant la distinction entre les chemins d'accès non valides non ignorables et les problèmes de système de fichiers ignorables. Sans surprise, cela est critique sous Windows. ( Voir ci-dessous.)
  • Ignorer les conditions de concurrence résultant de processus externes en même temps que le (re) déplacement des répertoires parents du chemin à tester. ( Voir ci-dessous.)
  • Ignorer les délais d'attente de connexion résultant de ce chemin d'accès résidant sur des systèmes de fichiers obsolètes, lents ou temporairement inaccessibles. Ceci pourrait exposer les services destinés au public à des attaques potentielles DoS -. ( Voir ci-dessous.)

Nous allons réparer tout ça.

Question n ° 0: Quelle est la validité du nom de chemin?

Avant de jeter nos combinaisons de viande fragiles dans les souffles de douleur criblés par le python, nous devrions probablement définir ce que nous entendons par "validité du nom de chemin". Qu'est-ce qui définit la validité, exactement?

Par "validité du nom de chemin", nous entendons la exactitude syntaxique d’un chemin par rapport au système de fichiers racine du système actuel - indépendamment du fait que ce chemin ou ses répertoires parents existent physiquement. Un nom de chemin est syntaxiquement correct selon cette définition s'il est conforme à toutes les exigences syntaxiques du système de fichiers racine.

Par "système de fichiers racine", nous entendons:

  • Sur les systèmes compatibles POSIX, le système de fichiers est monté dans le répertoire racine (/).
  • Sous Windows, le système de fichiers monté sur %HOMEDRIVE%, La lettre de lecteur avec suffixe du colon contenant l'installation actuelle de Windows (généralement mais pas nécessairement C:).

La signification de "correction syntaxique" dépend à son tour du type de système de fichiers racine. Pour les systèmes de fichiers ext4 (Et la plupart mais pas tous compatibles POSIX, un chemin est syntaxiquement correct si et seulement si ce chemin:

  • Ne contient pas d'octets nuls (c'est-à-dire, \x00 En Python). Ceci est une nécessité absolue pour tous les systèmes de fichiers compatibles POSIX.
  • Ne contient aucun composant de chemin d'accès de plus de 255 octets (par exemple, 'a'*256 En Python). Un composant de chemin est la plus longue sous-chaîne d'un chemin d'accès ne contenant pas de caractère / (Par exemple, bergtatt, ind, i et fjeldkamrene. dans le chemin /bergtatt/ind/i/fjeldkamrene).

Exactitude syntaxique. Système de fichiers racine. C'est ça.

Question n ° 1: Comment allons-nous faire la validité du nom de chemin?

Valider les chemins d'accès dans Python est étonnamment non intuitif. Je suis entièrement d'accord avec Fake Name ici: le paquet officiel os.path Devrait fournir une sortie. Solution idéale pour cela. Pour des raisons inconnues (et probablement peu convaincantes), ce n'est pas le cas. Heureusement, dérouler votre propre solution ad-hoc n'est pas ça vous déchirer. ..

O.K., C'est en fait. C'est poilu; c'est dégueulasse; il rit probablement quand il cogne et rit quand il brille. Mais qu'est-ce que tu vas faire? Nuthin '.

Nous allons bientôt descendre dans l'abîme radioactif du code de bas niveau. Mais d'abord, parlons magasin de haut niveau. Les fonctions standard os.stat() et os.lstat() déclenchent les exceptions suivantes lorsqu'elles transmettent des noms de chemin non valides:

  • Pour les noms de chemin résidant dans des répertoires non existants, les instances de FileNotFoundError.
  • Pour les noms de chemin résidant dans des répertoires existants:
    • Sous Windows, les instances de WindowsError dont l'attribut winerror est 123 (C'est-à-dire, ERROR_INVALID_NAME).
    • Sous tous les autres systèmes d'exploitation:
    • Pour les noms de chemin contenant des octets nuls (c'est-à-dire, '\x00'), Les instances de TypeError.
    • Pour les noms de chemin contenant des composants de chemin d'accès supérieurs à 255 octets, les instances de OSError dont l'attribut errcode est:
      • Sous SunOS et la famille de systèmes d'exploitation * BSD, errno.ERANGE. (Cela semble être un bogue au niveau du système d'exploitation, aussi appelé "interprétation sélective" de la norme POSIX.)
      • Sous tous les autres systèmes d'exploitation, errno.ENAMETOOLONG.

Ceci implique que seuls les noms de chemin résidant dans des répertoires existants puissent être validés. Les fonctions os.stat() et os.lstat() generic FileNotFoundError exceptions lorsque des noms de chemin d'accès passés dans des répertoires non existants sont passés, que ces noms de chemin soient invalides ou non. L'existence de répertoire prime sur l'invalidité du nom de chemin.

Cela signifie-t-il que les noms de chemin résidant dans des répertoires non existants sont pas validables? Oui, sauf si nous modifions ces noms de chemin pour qu'ils résident dans des répertoires existants. Est-ce même faisable en toute sécurité, cependant? La modification d'un nom de chemin ne devrait-elle pas nous empêcher de valider le chemin d'origine?

Pour répondre à cette question, rappelez ci-dessus que les noms de chemin syntaxiquement corrects du système de fichiers ext4 Ne contiennent aucun composant de chemin (A) contenant des octets nuls ou (B) d'une longueur supérieure à 255 octets. Par conséquent, un chemin d'accès ext4 Est valide si et seulement si tous les composants de chemin d'accès de ce chemin d'accès sont valides. Ceci est vrai de mostsystèmes de fichiers réels d'intérêt.

Est-ce que cette vision pédante nous aide réellement? Oui. Cela réduit le problème plus général de validation du nom de chemin complet d'un seul coup au problème plus petit de ne valider que tous les composants de chemin de ce chemin. Tout nom de chemin arbitraire peut être validé (qu'il soit ou non dans un répertoire existant) de manière multiplateforme en suivant l'algorithme suivant:

  1. Divisez ce chemin en composants de chemin (par exemple, le chemin /troldskog/faren/vild Dans la liste ['', 'troldskog', 'faren', 'vild']).
  2. Pour chacun de ces composants:
    1. Joignez le nom de chemin d’un répertoire garanti d’exister avec ce composant dans un nouveau nom de chemin temporaire (par exemple, /troldskog).
    2. Transmettez ce chemin à os.stat() ou os.lstat(). Si ce nom de chemin et par conséquent ce composant est invalide, il est garanti que cet appel lève une exception exposant le type d'invalidité plutôt qu'une exception générique FileNotFoundError. Pourquoi? Parce que ce nom de chemin réside dans un répertoire existant. (La logique circulaire est circulaire.)

Existe-t-il un répertoire garanti? Oui, mais en général un seul: le répertoire le plus élevé du système de fichiers racine (tel que défini ci-dessus).

Passer des noms de chemins résidant dans tout autre répertoire (et par conséquent non garantis) vers os.stat() ou os.lstat() invite à des conditions de concurrence, même si ce répertoire a déjà été testé. Pourquoi? Parce que des processus externes ne peuvent pas être empêchés de supprimer simultanément ce répertoire après ce test a été effectué, mais avant ce chemin est passé à os.stat() ou os.lstat(). Libérez les chiens de la folie fétiche!

L’approche ci-dessus présente également un avantage important: la sécurité . (N’est-ce pas que gentil?) Plus précisément:

Les applications frontales validant des noms de chemin arbitraires provenant de sources non fiables en transmettant simplement ces noms de chemin à os.stat() ou os.lstat() sont susceptibles d'attaques par déni de service (DoS) et autres manigances à chapeau noir. Les utilisateurs malveillants peuvent tenter de valider de manière répétée les noms de chemins résidant sur des systèmes de fichiers réputés obsolètes ou autrement lents (par exemple, les partages NFS Samba); dans ce cas, déclencher aveuglément des noms de chemin entrants risque d’échouer éventuellement avec des délais de connexion ou de prendre plus de temps et de ressources que votre faible capacité à résister au chômage.

L'approche ci-dessus permet d'éviter cela en ne validant que les composants de chemin d'un chemin d'accès par rapport au répertoire racine du système de fichiers racine. (Si même c'est obsolète, lent ou inaccessible, vous avez des problèmes plus importants que la validation du nom de chemin.)

Perdu? Génial. Commençons. (Python 3 supposé. Voir "Qu'est-ce que l'espoir fragile pour 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 (e.g., `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 (e.g., 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 (e.g., 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" (i.e., no such file or directory) rather
            #   than "ENAMETOOLONG" (i.e., 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 (e.g., 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
    # (e.g., a bug). Permit this exception to unwind the call stack.
    #
    # Did we mention this should be shipped with Python already?

Fait. Ne louchez pas ce code. ( ça mord.)

Question n ° 2: Existence ou possibilité de création de chemin d'accès potentiellement non valide, hein?

Tester l'existence ou la possibilité de création de chemins d'accès éventuellement non valides est, pour la plupart des cas, trivial. La petite clé ici est d'appeler la fonction précédemment définie avant tester le chemin passé:

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 (e.g., 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

Fait et fait. Sauf que pas tout à fait.

Question n ° 3: Existence ou possibilité d'inscriptibilité éventuellement incorrecte du nom de chemin sous Windows

Il existe une mise en garde. Bien sûr il fait.

Comme l’officiel os.access() documentation admet:

Remarque: Les opérations d'E/S peuvent échouer même lorsque os.access() indique qu'elles réussiraient, en particulier pour les opérations sur les systèmes de fichiers réseau susceptibles d'avoir sémantique des autorisations au-delà du modèle POSIX habituel.

Personne n’est surpris, Windows est ici le suspect habituel. Grâce à l'utilisation intensive des listes de contrôle d'accès (ACL) sur les systèmes de fichiers NTFS, le modèle simpliste POSIX avec permission-bit correspond mal à la réalité Windows sous-jacente. Bien que cela ne soit (vraisemblablement) pas de la faute de Python, cela pourrait néanmoins poser problème pour les applications compatibles Windows.

Si c'est vous, une alternative plus robuste est recherchée. Si le chemin d'accès passé est pas, nous essayons plutôt de créer un fichier temporaire dont le remplacement immédiat dans le répertoire parent de ce chemin est garanti - un test de la créabilité plus portable (si coûteux):

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.
    '''
    # 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 (e.g., 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

Notez cependant que même this peut ne pas suffire.

Grâce au contrôle d’accès des utilisateurs (UAC), à l’inimaginable Windows Vista et à toutes ses itérations ultérieures mensonge flagrant à propos des autorisations relatives aux répertoires système. Lorsque des utilisateurs non administrateurs tentent de créer des fichiers dans les répertoires canonique C:\Windows Ou C:\Windows\system32, Le contrôle de compte d'utilisateur autorise superficiellement l'utilisateur à le faire, tandis que en fait en isolant tous les fichiers créés. fichiers dans un "magasin virtuel" dans le profil de cet utilisateur. (Qui aurait pu imaginer que tromper les utilisateurs aurait des conséquences néfastes à long terme?)

C'est fou. C'est Windows.

Prouve le

Osons-nous? Il est temps de tester les tests ci-dessus.

Etant donné que NULL est le seul caractère interdit dans les noms de chemins sur les systèmes de fichiers orientés UNIX, profitons de cela pour démontrer la dure vérité: ignorer les manigances non négligeables de Windows, qui m'ennuient et me mettent en colère de la même manière:

>>> 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

Au-delà de la santé mentale. Au-delà de la douleur. Vous trouverez Python des problèmes de portabilité.

105
Cecil Curry
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

Notez que path.exists peut échouer pour plus de raisons que the file is not there, vous devrez peut-être effectuer des tests plus précis, comme par exemple vérifier si le répertoire contenant existe.


Après ma discussion avec l'OP, il s'est avéré que le principal problème semble être que le nom du fichier peut contenir des caractères non autorisés par le système de fichiers. Bien sûr, ils doivent être supprimés, mais le PO veut maintenir autant de ressources humaines que le système de fichiers le permet.

Malheureusement, je ne connais aucune bonne solution pour cela. Cependant réponse de Cecil Curry examine de plus près la détection du problème.

34
Nobody

Avec Python 3, que diriez-vous:

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

Avec l'option 'x', nous n'avons pas à nous soucier des conditions de course. Voir la documentation ici .

Maintenant, cela va créer un fichier temporaire très court s'il n'existe pas déjà - à moins que le nom ne soit invalide. Si vous pouvez vivre avec cela, cela simplifiera beaucoup les choses.

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

ouvrira le fichier ou donnera une erreur s’il n’existe pas. S'il y a une erreur, vous pouvez alors essayer d'écrire sur le chemin. Sinon, vous obtenez une deuxième erreur.

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

Regardez aussi ici à propos des permissions sur les fenêtres

4
vikki