簡體   English   中英

在不在路徑目標處創建文件的情況下檢查路徑在 Python 中是否有效

[英]Check whether a path is valid in Python without creating a file at the path's target

我有一個路徑(包括目錄和文件名)。
我需要測試文件名是否有效,例如文件系統是否允許我創建具有這樣名稱的文件。
文件名中有一些 unicode 字符

可以安全地假設路徑的目錄段是有效的和可訪問的(我試圖使這個問題更普遍適用,但顯然我太過分了)。

除非必須,否則我非常不想逃避任何事情。

我會發布一些我正在處理的示例字符,但顯然它們會被堆棧交換系統自動刪除。 無論如何,我想保留像ö這樣的標准 unicode 實體,並且只轉義文件名中無效的內容。


這是陷阱。 路徑的目標可能(也可能不)已經有一個文件。 如果該文件存在,我需要保留該文件,如果不存在,則不創建文件。

基本上我想檢查我是否可以不實際打開寫入路徑的情況下寫入路徑(以及通常需要的自動文件創建/文件破壞)。

像這樣:

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創建文件,然后我必須將其os.unlink

最后,它似乎花費了 6 或 7 行來做一些應該像os.isvalidpath(filePath)或類似的一樣簡單的事情。


順便說一句,我需要它在(至少)Windows 和 MacOS 上運行,所以我想避免特定於平台的東西。

``

tl;博士

調用下面定義的is_path_exists_or_creatable()函數。

嚴格來說是 Python 3。這就是我們滾動的方式。

兩個問題的故事

“我如何測試路徑名有效性,以及對於有效路徑名,這些路徑的存在或可寫性”的問題? 顯然是兩個不同的問題。 兩者都很有趣,而且都沒有在這里得到真正令人滿意的答案......或者,我可以 grep 的任何地方

vikki答案可能是最接近的,但具有以下顯着缺點:

  • 不必要地打開( ...然后未能可靠地關閉)文件句柄。
  • 不必要地寫入( ...然后無法可靠地關閉或刪除)0 字節文件。
  • 忽略區分不可忽略的無效路徑名和可忽略的文件系統問題的特定於操作系統的錯誤。 不出所料,這在 Windows 下很重要。 見下文。
  • 忽略由外部進程同時(重新)移動要測試的路徑名的父目錄導致的競爭條件。 見下文。
  • 忽略由於此路徑名駐留在陳舊、緩慢或以其他方式暫時無法訪問的文件系統上而導致的連接超時。 可能會將面向公眾的服務暴露給潛在的DoS驅動的攻擊。 見下文。

我們要解決這一切。

問題 #0:什么是路徑名有效性?

在將我們脆弱的肉服扔進充滿蟒蛇的痛苦之前,我們可能應該定義“路徑名有效性”的含義。 究竟是什么定義了有效性?

“路徑名有效性”是指路徑名相對於當前系統的根文件系統句法正確性——無論該路徑或其父目錄是否物理存在。 如果路徑名符合根文件系統的所有語法要求,則在此定義下它在語法上是正確的。

通過“根文件系統”,我們的意思是:

  • 在 POSIX 兼容系統上,文件系統掛載到根目錄 ( / )。
  • 在Windows中,文件系統安裝到%HOMEDRIVE%包含當前的Windows安裝冒號后綴驅動器號(通常但不一定C:

反過來,“語法正確性”的含義取決於根文件系統的類型。 對於ext4 (以及大多數但不是所有 POSIX 兼容的)文件系統,當且僅當該路徑名在語法上是正確的:

  • 不包含空字節(即 Python 中的\\x00 )。 這是所有 POSIX 兼容文件系統的硬性要求。
  • 不包含長度超過 255 字節的路徑組件(例如,Python 中的'a'*256 )。 路徑成分是含有不路徑名的最長子串/字符(例如, bergtattindifjeldkamrene在路徑名/bergtatt/ind/i/fjeldkamrene )。

句法正確。 根文件系統。 就是這樣。

問題 1:我們現在應該如何進行路徑名有效性?

在 Python 中驗證路徑名非常不直觀。 我在這里與Fake Name一致:官方os.path包應該為此提供開箱即用的解決方案。 由於未知(並且可能沒有說服力)的原因,它沒有。 幸運的是,展開您自己的臨時解決方案並不是那么令人痛苦……

好吧,確實如此。 毛茸茸的; 這很糟糕; 它可能會在發出咕嚕聲時發出咯咯聲,在發光時發出咯咯聲。 但是你要做什么? Nuthin'。

我們很快就會陷入低級代碼的放射性深淵。 但首先,讓我們談談高級商店。 當傳遞無效路徑名時,標准os.stat()os.lstat()函數會引發以下異常:

  • 對於駐留在不存在的目錄中的路徑名, FileNotFoundError實例。
  • 對於駐留在現有目錄中的路徑名:
    • 在 Windows 下, winerror屬性為123 (即ERROR_INVALID_NAME )的WindowsError實例。
    • 在所有其他操作系統下:
    • 對於包含空字節(即'\\x00' )的路徑名, TypeError實例。
    • 對於包含長度超過 255 字節的路徑組件的路徑名,其errcode屬性為的OSError實例:
      • 在 SunOS 和 *BSD 系列操作系統下, errno.ERANGE (這似乎是操作系統級別的錯誤,也稱為 POSIX 標准的“選擇性解釋”。)
      • 在所有其他操作系統下, errno.ENAMETOOLONG

至關重要的是,這意味着只有駐留在現有目錄中的路徑名是可驗證的。 當傳遞的路徑名位於不存在的目錄中時, os.stat()os.lstat()函數會引發通用FileNotFoundError異常,無論這些路徑名是否無效。 目錄存在優先於路徑名無效。

這是否意味着駐留在不存在目錄中的路徑名不可驗證? 是的 - 除非我們修改這些路徑名以駐留在現有目錄中。 然而,這甚至安全可行嗎? 修改路徑名不應該阻止我們驗證原始路徑名嗎?

要回答這個問題,請回憶上面的ext4文件系統上語法正確的路徑名不包含路徑組件(A)包含空字節或(B)長度超過 255 個字節。 因此,當且僅當該路徑名中的所有路徑組件都有效時, ext4路徑名才有效。 對於大多數感興趣的現實世界文件系統來說都是如此。

這種迂腐的見解真的對我們有幫助嗎? 是的。 它將一舉驗證完整路徑名的較大問題減少到僅驗證該路徑名中的所有路徑組件的較小問題。 任何任意路徑名都可以通過遵循以下算法以跨平台方式驗證(無論該路徑名是否駐留在現有目錄中):

  1. 將該路徑名拆分為路徑組件(例如,路徑名/troldskog/faren/vild到列表['', 'troldskog', 'faren', 'vild'] )。
  2. 對於每個這樣的組件:
    1. 將保證與該組件一起存在的目錄的路徑名加入新的臨時路徑名(例如, /troldskog )。
    2. 將該路徑名傳遞給os.stat()os.lstat() 如果該路徑名以及該組件無效,則此調用肯定會引發一個異常,以暴露無效類型,而不是通用FileNotFoundError異常。 為什么? 因為該路徑名駐留在現有目錄中。 (循環邏輯是循環的。)

是否有保證存在的目錄? 是的,但通常只有一個:根文件系統的最頂層目錄(如上定義)。

將駐留在任何其他目錄(因此不保證存在)中的路徑名傳遞給os.stat()os.lstat()引發競爭條件,即使該目錄先前已被測試存在。 為什么? 因為在執行該測試之后將該路徑名傳遞給os.stat()os.lstat()之前,無法阻止外部進程同時刪除該目錄。 釋放精神錯亂的狗!

上述方法還有一個很大的附帶好處:安全性。 不是很好嗎?)具體為:

通過簡單地將此類路徑名傳遞給os.stat()os.lstat()來驗證來自不受信任來源的任意路徑名的前端應用程序容易受到拒絕服務 (DoS) 攻擊和其他黑帽惡作劇的影響。 惡意用戶可能會嘗試重復驗證駐留在已知陳舊或緩慢的文件系統(例如,NFS Samba 共享)上的路徑名; 在這種情況下,盲目地統計傳入的路徑名很可能最終因連接超時而失敗,或者比您承受失業的微弱能力消耗更多的時間和資源。

上述方法通過僅根據根文件系統的根目錄驗證路徑名的路徑組件來避免這種情況。 (如果即使那是陳舊的、緩慢的或無法訪問的,你也會遇到比路徑名驗證更大的問題。)

丟失? 偉大的。 讓我們開始。 (假設 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://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.
    '''
    # 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?

完畢。 不要斜視那個代碼。 它咬人。

問題 2:可能無效的路徑名存在或可創建性,嗯?

鑒於上述解決方案,測試可能無效的路徑名的存在或可創建性大多是微不足道的。 這里的小關鍵是測試傳遞的路徑之前調用之前定義的函數:

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()文檔所承認的:

注意:即使os.access()表明它們會成功,I/O 操作也可能會失敗,特別是對於網絡文件系統上的操作,其權限語義可能超出通常的 POSIX 權限位模型。

不出所料,Windows 是這里的常見嫌疑人。 由於在 NTFS 文件系統上廣泛使用訪問控制列表 (ACL),簡單的 POSIX 權限位模型很難映射到底層的 Windows 現實。 雖然這(可以說)不是 Python 的錯,但對於 Windows 兼容的應用程序來說,它可能仍然值得關注。

如果這是您,則需要更強大的替代方案。 如果傳遞的路徑存在,我們不是試圖建立保證該路徑的父目錄被立即刪除臨時文件- creatability的更便攜的(如昂貴的)測試:

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

但是請注意,即使這樣可能還不夠。

多虧了用戶訪問控制 (UAC),永遠無法模仿的 Windows Vista 及其所有后續迭代公然謊稱與系統目錄有關的權限。 當非管理員用戶嘗試在規范的C:\\WindowsC:\\Windows\\system32目錄中創建文件時,UAC 表面上允許用戶這樣做,同時實際將所有創建的文件隔離到該用戶配置文件中的“虛擬存儲”中. (誰能想到欺騙用戶會產生有害的長期后果?)

這太瘋狂了。 這是 Windows。

證明給我看

我們敢嗎? 是時候試駕上述測試了。

由於 NULL 是面向 UNIX 的文件系統上的路徑名中唯一禁止的字符,讓我們利用它來證明冷酷的事實——忽略不可忽視的 Windows 惡作劇,坦率地說,這同樣讓我感到厭煩和憤怒:

>>> 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 可移植性問題。

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 的回答仔細研究了檢測問題。

使用 Python 3,如何:

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

使用 'x' 選項,我們也不必擔心競爭條件。 請參閱此處的文檔。

現在,如果它不存在,這將創建一個非常短暫的臨時文件 - 除非名稱無效。 如果你能接受它,它會簡化很多事情。

找到一個名為 pathvalidate 的 PyPI 模塊

https://pypi.org/project/pathvalidate/

pip 安裝路徑驗證

它內部有一個名為 sanitize 的函數,它將獲取文件路徑並將其轉換為有效的 filePath

from pathvalidate import sanitize_filepath
file1 = “ap:lle/fi:le”
print(sanitize_filepath(file1))
#will return apple/file

它也適用於保留名稱。 如果你給它 filePath con,它會返回 con_

因此,有了這些知識,我們就可以檢查輸入的文件路徑是否等於經過消毒的文件路徑,這意味着文件路徑是有效的

import os
from pathvalidate import sanitize_filepath

def check(filePath):
    if os.path.exisits(filePath):
        return True
    if filePath == sanitize_filepath(filePath):
        return True
    return False
open(filename,'r')   #2nd argument is r and not w

如果文件不存在,將打開文件或給出錯誤。 如果有錯誤,那么你可以嘗試寫入路徑,如果你不能,那么你會得到第二個錯誤

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

還可以在這里查看有關 Windows 權限的信息

嘗試os.path.exists這將檢查路徑,如果存在則返回True否則返回False

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM