web-dev-qa-db-ja.com

パスのターゲットにファイルを作成せずに、Pythonでパスが有効かどうかを確認します

パス(ディレクトリとファイル名を含む)があります。
ファイル名が有効かどうかをテストする必要があります。ファイルシステムでこのような名前のファイルを作成できる場合。
ファイル名には、Unicode文字が含まれています。

パスのディレクトリセグメントが有効でアクセス可能であると想定しても安全です(私は質問をより一般的に適用できるようにしようとしていたが、明らかに私は遠すぎた)。

haveにしない限り、何もエスケープする必要はありません。

私が扱っているサンプル文字のいくつかを投稿したいと思いますが、どうやらそれらはスタック交換システムによって自動的に削除されます。とにかく、_ö_のような標準のUnicodeエンティティを保持し、ファイル名で無効なものだけをエスケープしたいです。


これがキャッチです。 パスのターゲットに既にファイルがある(またはない)可能性があります。存在する場合はそのファイルを保持し、存在しない場合はファイルを作成しません。

基本的に、couldにパスを書き込むかどうかを確認したい書き込み用のパスを実際に開かずに(および、通常は自動ファイル作成/を伴います)。

など:

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

ここから

既存のファイルを上書きするので、これは受け入れられません。既存のファイルは(存在する場合は)触れたくないか、存在しない場合はそのファイルを作成します。

私ができることを知っています:

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

しかし、それはfilePathにあるファイルをcreateするので、_os.unlink_にする必要があります。

結局、os.isvalidpath(filePath)のような簡単なことをするために6行か7行を費やしているように見えます。


余談ですが、(少なくとも)WindowsとMacOSで実行するにはこれが必要なので、プラットフォーム固有のものを避けたいと思います。

68
Fake Name

tl; dr

以下で定義されているis_path_exists_or_creatable()関数を呼び出します。

厳密にPython 3.これがまさにロールバックです。

2つの質問の物語

「パス名の有効性、および有効なパス名について、それらのパスの存在または書き込み可能性をテストするにはどうすればよいですか?」という質問明らかに2つの別個の質問です。両方とも興味深いものであり、どちらも本当に満足のいく答えをここで受け取っていません...または、まあ、anywhere私はgrepすることができました。

vikkianswer おそらく最も近いものを切り開きますが、次のような顕著な欠点があります。

  • 不必要にファイルハンドルを開きます(...そして、確実に閉じることができません)。
  • 不要な書き込み(...そして信頼できるクローズまたは削除に失敗)0バイトのファイル。
  • 無視できない無効なパス名と無視できるファイルシステムの問題を区別するOS固有のエラーを無視します。当然のことながら、これはWindowsでは重要です。 (以下を参照。
  • テストするパス名の親ディレクトリを同時に(再)移動する外部プロセスから生じる競合状態を無視します。 (以下を参照。
  • 古い、遅い、または一時的にアクセスできないファイルシステムに存在するこのパス名に起因する接続タイムアウトを無視します。これはcould公開サービスを潜在的な攻撃にさらす DoS -driven攻撃です。 (以下を参照。

それをすべて修正します。

質問#0:パス名の有効性とは何ですか?

私たちの壊れやすい肉のスーツを、Pythonでいっぱいの痛みのモッシュピットに投げ込む前に、「パス名の有効性」が意味することを定義する必要があります。有効性を正確に定義するものは何ですか?

「パス名の有効性」とは、現在のシステムの rootファイルシステムに対するパス名の構文の正確さを意味します。そのパスまたはその親ディレクトリは物理的に存在します。ルートファイルシステムのすべての構文要件に準拠している場合、この定義ではパス名は構文的に正しいです。

「ルートファイルシステム」とは、次のことを意味します。

  • POSIX互換システムでは、ファイルシステムはルートディレクトリ(/)にマウントされます。
  • Windowsでは、ファイルシステムは%HOMEDRIVE%、現在のWindowsインストールを含むコロンで区切られたドライブ文字にマウントされます(通常はnot必然的にC:)。

「構文の正しさ」の意味は、ルートファイルシステムのタイプに依存します。 ext4(およびほとんどのnotすべてのPOSIX互換)ファイルシステムの場合、パス名はそのパス名の場合にのみ構文的に正しい:

  • Nullバイトは含まれていません(つまり、Pythonの\x00)。 これはすべてのPOSIX互換ファイルシステムのハード要件です。
  • 255バイトより長いパスコンポーネントは含まれていません(例:Pythonの'a'*256)。パスコンポーネントは、/文字を含まないパス名の最長部分文字列です(例:bergtattindi、およびfjeldkamreneは、パス名/bergtatt/ind/i/fjeldkamrene)。

構文の正確さ。ルートファイルシステム。それでおしまい。

質問1:パス名の有効性を確認する方法

Pythonでのパス名の検証は驚くほど直感的ではありません。私は Fake Name に同意します:公式os.pathパッケージはout-未知の(そしておそらく説得力のない)理由で、それはありません。幸いなことに、独自のアドホックソリューションを展開することはthat gut-wrenching ...ではありません.

O.K。、実際はそうです。それは毛深いです。それは厄介です。それはおそらく、それが光るにつれて、ゴロゴロとくすくす笑いながら鳴ります。しかし、あなたは何をしますか? Nuthin '。

すぐに、低レベルのコードの放射性の深aに降ります。しかし、最初に、高級店について話しましょう。標準のos.stat()およびos.lstat()関数は、無効なパス名が渡されると、次の例外を発生させます。

  • 存在しないディレクトリにあるパス名の場合、FileNotFoundErrorのインスタンス。
  • 既存のディレクトリにあるパス名の場合:
    • Windowsでは、WindowsError属性が123(つまり、ERROR_INVALID_NAME)であるwinerrorのインスタンス。
    • 他のすべてのOSの場合:
    • Nullバイトを含むパス名(つまり、'\x00')、TypeErrorのインスタンス。
    • 255バイトより長いパスコンポーネントを含むパス名の場合、OSError属性のインスタンスはerrcodeのインスタンスは次のとおりです。
      • SunOSおよびOSの* BSDファミリでは、errno.ERANGE。 (これは、OSレベルのバグのようです。それ以外の場合は、POSIX標準の「選択的解釈」と呼ばれます。)
      • 他のすべてのOSでは、errno.ENAMETOOLONG

重要なのは、既存のディレクトリに存在するパス名のみが検証可能であることを意味します。os.stat()およびos.lstat()関数は、存在しないディレクトリに存在するパス名を渡されたときに、一般的なFileNotFoundErrorそれらのパス名が無効かどうか。ディレクトリの存在は、パス名の無効性よりも優先されます。

これは、存在しないディレクトリにあるパス名がnot検証可能であるということですか?はい-既存のディレクトリに存在するようにこれらのパス名を変更しない限り。しかし、それは安全に実行可能ですか?パス名を変更しても、元のパス名の検証が妨げられるべきではありませんか?

この質問に答えるには、ext4ファイルシステムの構文的に正しいパス名にはパスコンポーネントが含まれていないことを思い出してください(A) nullバイトまたは(B )長さが255バイトを超えています。したがって、ext4パス名は、そのパス名のすべてのパスコンポーネントが有効な場合にのみ有効です。これはmost現実のファイルシステム に該当します。

その教訓的な洞察は実際に私たちを助けますか?はい。完全なパス名を一度に検証するという大きな問題を、そのパス名のすべてのパスコンポーネントのみを検証するという小さな問題に減らします。任意のパス名は、次のアルゴリズムに従うことにより、クロスプラットフォームで(そのパス名が既存のディレクトリに存在するかどうかに関係なく)有効です。

  1. そのパス名をパスコンポーネントに分割します(たとえば、パス名/troldskog/faren/vildをリスト['', 'troldskog', 'faren', 'vild']に)。
  2. そのようなコンポーネントごとに:
    1. そのコンポーネントとともに存在することが保証されているディレクトリのパス名を、新しい一時パス名(例:/troldskog)に結合します。
    2. そのパス名をos.stat()またはos.lstat()に渡します。そのパス名、したがってそのコンポーネントが無効な場合、この呼び出しは、一般的なFileNotFoundError例外ではなく、無効性のタイプを公開する例外を発生させることが保証されています。どうして? そのパス名は既存のディレクトリにあるため。(循環ロジックは循環的です。)

存在が保証されているディレクトリはありますか?はい、ただし通常は1つのみ:ルートファイルシステムの最上位ディレクトリ(上記で定義)。

他のディレクトリに存在する(したがって、存在が保証されない)パス名をos.stat()またはos.lstat()に渡すと、そのディレクトリが以前に存在することがテストされていたとしても、競合状態になります。どうして?外部プロセスがそのディレクトリを同時に削除することを防ぐことはできないためafterテストは実行されましたがbeforeそのパス名はos.stat()またはos.lstat()に渡されます。心を奪う狂気の犬を解き放ちましょう!

上記のアプローチには、実質的な副次的な利点もあります:security.(Is n't that nice?)具体的には:

信頼できないソースからの任意のパス名を単純にos.stat()またはos.lstat()に渡すことで検証する前面アプリケーションは、サービス拒否(DoS)攻撃やその他のブラックハット攻撃の影響を受けやすくなります。悪意のあるユーザーは、古くなっているか低速であることがわかっているファイルシステム(NFS Samba共有など)に存在するパス名を繰り返し検証しようとする場合があります。その場合、やみくもに着信パス名を設定すると、最終的に接続タイムアウトで失敗するか、失業に耐えるための弱い能力よりも多くの時間とリソースを消費します。

上記のアプローチは、ルートファイルシステムのルートディレクトリに対してパス名のパスコンポーネントのみを検証することにより、これを回避します。 (もしthat's古い、遅い、またはアクセスできない場合、パス名の検証よりも大きな問題があります。)

失った? すばらしい。始めましょう。 (Python 3が想定されています。「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?

Done.そのコードに目を凝らさないでください。 (噛む。

質問#2:パス名の存在または作成可能性が無効である可能性がありますか?

おそらく無効なパス名の存在または作成可能性をテストすることは、上記のソリューションを考えると、たいてい些細なことです。ここでの小さなキーは、以前に定義された関数を呼び出すことですbefore渡されたパスをテストします:

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

完了および完了。.

質問#3:Windows上で無効なパス名の存在または書き込み可能性

警告があります。もちろんあります。

公式 os.access() documentation として認める:

注: I/O操作は、os.access()が成功することを示す場合でも失敗する場合があります。特に、通常のPOSIX許可ビットモデルを超える許可セマンティクスを持つネットワークファイルシステムの操作の場合。

驚くことではないが、Windowsはここでの通常の容疑者です。 NTFSファイルシステムでのアクセス制御リスト(ACL)の広範囲な使用のおかげで、単純なPOSIX許可ビットモデルは、基盤となるWindowsの現実にほとんど対応していません。これは(ほぼ間違いなく)Pythonのせいではありませんが、それでもWindows互換のアプリケーションにとっては心配になるかもしれません。

これがあなたなら、より堅牢な代替手段が必要です。渡されたパスがnotである場合、代わりにそのパスの親ディレクトリですぐに削除されることが保証されている一時ファイルを作成しようとします。

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

ただし、thisでも十分ではないことに注意してください。

ユーザーアクセス制御(UAC)、これまでにないWindows Vista、およびそれ以降のすべての繰り返しのおかげで、システムディレクトリに関連するアクセス許可について blalently lie です。管理者以外のユーザーが正規のC:\WindowsまたはC:\Windows\system32ディレクトリのいずれかにファイルを作成しようとすると、UACは実際に作成されたすべてのファイルを "そのユーザーのプロファイルの「仮想ストア」。 (だれがユーザーを欺くことが有害な長期的な結果をもたらすと想像できたでしょうか?)

狂ってる。これはWindowsです。

証明する

あえて?上記のテストを試してみましょう。

NULLはUNIX指向のファイルシステムのパス名で禁止されている唯一の文字であるため、それを活用して冷静で難しい真実を実証しましょう。

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

正気を超えて。痛みを超えて。 Python移植性の問題が見つかります。

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

ご了承ください path.existsは、単にthe file is not thereしたがって、含まれるディレクトリが存在するかどうかのテストなど、より細かいテストを行う必要がある場合があります。


OPとの議論の結果、主な問題は、ファイル名にファイルシステムで許可されていない文字が含まれている可能性があることが判明したことです。もちろん削除する必要がありますが、OPはファイルシステムが許す限り人間の読み取り能力を維持したいと考えています。

悲しいことに、私はこれに対する良い解決策を知りません。ただし、Cecil Curryの答えは、問題の検出について詳しく調べます。

34
Nobody

Python 3、どうですか:

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

「x」オプションを使用すると、競合状態を心配する必要もありません。ドキュメントを参照してください こちら

現在、名前が無効でない限り、これは非常に短命の一時ファイルを作成します(まだ存在しない場合)。それと一緒に暮らすことができれば、物事が非常に簡単になります。

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

ファイルを開くか、存在しない場合はエラーを返します。エラーがある場合は、パスへの書き込みを試みることができます。できない場合は、2番目のエラーが発生します

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

また、 こちら Windowsの権限について見てください

4
vikki