检查路径在Python中是否有效,而不在路径的目标上创建文件

时间:2012-03-02 11:26:38

标签: python filesystems filepath

我有一条路径(包括目录和文件名) 我需要测试文件名是否有效,例如如果文件系统允许我创建一个具有这样名称的文件 文件名中包含一些unicode字符

可以安全地假设路径的目录段是有效且可访问的(我试图使问题更加适用,显然我太过分了)。

除非我拥有,否则我非常不想逃避任何事情。

我发布了一些我正在处理的示例字符,但显然它们会被堆栈交换系统自动删除。无论如何,我想保留像ö这样的标准unicode实体,并且只保留文件名中无效的东西。


这是捕获。 可能(或可能不)已经是路径目标的文件。如果文件确实存在,我需要保留该文件,如果文件不存在则不需要创建文件。

基本上我想检查我是否可以写入路径而不实际打开写入路径(以及通常需要的自动文件创建/文件clobbering)。

因此:

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

from 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上运行,所以我想避免特定于平台的东西。

``

6 个答案:

答案 0 :(得分:96)

TL;博士

调用下面定义的is_path_exists_or_creatable()函数。

严格的Python 3.这就是我们如何滚动。

两个问题的故事

问题"我如何测试路径名的有效性,以及对于有效的路径名,这些路径的存在性或可写性?"显然是两个独立的问题。两者都很有趣,而且在这里都没有得到真正令人满意的答案......或者, 我可以grep。

vikki'} answer可能是最接近的,但有明显的缺点:

  • 不必要地打开( ...然后无法可靠地关闭)文件句柄。
  • 不必要地写( ...然后无法可靠关闭或删除)0字节文件。
  • 忽略特定于操作系统的错误,区分不可忽略的无效路径名和可忽略的文件系统问题。不出所料,这在Windows下至关重要。 (见下文。
  • 忽略由外部进程同时(重新)移动要测试的路径名的父目录导致的竞争条件。 (见下文。
  • 忽略由此路径名导致的连接超时,这些路径名驻留在过时,慢速或其他临时无法访问的文件系统上。此可能将面向公众的服务暴露给潜在的DoS驱动的攻击。 (见下文。

我们要解决所有问题。

问题#0:路径名的有效性又是什么?

在将我们脆弱的肉类套装投入到痛苦的蟒蛇群体之前,我们应该定义我们所说的“路径名有效性”。"究竟是什么定义了有效性?

通过"路径名有效性,"我们指的是相对于当前系统的根文件系统的路径名的语法正确性 - 无论该路径或其父目录是否物理存在。如果路径名在符合根文件系统的所有语法要求的情况下在此定义下在语法上是正确的。

通过"根文件系统,"我们的意思是:

  • 在POSIX兼容系统上,文件系统挂载到根目录(/)。
  • 在Windows上,文件系统挂载到%HOMEDRIVE%,冒号后缀的驱动器号包含当前的Windows安装(通常但必须C:)。

"句法正确性的含义,"反过来,取决于根文件系统的类型。对于ext4(以及大多数所有与POSIX兼容的)文件系统,路径名在语法上是正确的,当且仅当该路径名为:

  • 不包含空字节(即Python中的\x00)。 这是所有与POSIX兼容的文件系统的硬性要求。
  • 不包含长度超过255个字节的路径组件(例如,Python中的'a'*256)。路径组件是路径名中不包含/字符的最长子字符串(例如,bergtattindifjeldkamrene,路径名为{{ 1}})。

句法正确性。根文件系统。那就是它。

问题#1:我们现在应该如何进行路径名有效性?

在Python中验证路径名非常不直观。我在这里与Fake Name达成了一致意见:官方/bergtatt/ind/i/fjeldkamrene软件包应为此提供开箱即用的解决方案。对于未知(并且可能没有说服力)的原因,它没有。幸运的是,展开你自己的临时解决方案并非 痛苦......

O.K。,它实际上是。它毛茸茸的;它很讨厌;它可能会随着它的发光而变得微不足道。但是你要做什么? Nuthin'

我们很快就会陷入低级代码的放射性深渊。但首先,让我们谈谈高级商店。传递无效路径名时,标准os.pathos.stat()函数会引发以下异常:

  • 对于驻留在不存在的目录中的路径名,os.lstat()
  • 的实例
  • 对于驻留在现有目录中的路径名:
    • 在Windows下,FileNotFoundError属性为WindowsError的{​​{1}}个实例(即winerror)。
    • 在所有其他操作系统下:
    • 对于包含空字节的路径名(即123),ERROR_INVALID_NAME的实例。
    • 对于包含长度超过255个字节的路径组件的路径名,'\x00'属性为TypeError的实例为:
      • 在SunOS和* BSD系列操作系统下,OSError。 (这似乎是操作系统级错误,否则称为"选择性解释" POSIX标准。)
      • 在所有其他操作系统下,errcode

至关重要的是,这意味着仅存在于现有目录中的路径名是可验证的。 errno.ERANGEerrno.ENAMETOOLONG函数在传递的路径名传递时会引发通用os.stat()异常不存在的目录,无论这些路径名是否无效。目录存在优先于路径名无效。

这是否意味着驻留在不存在的目录中的路径名可验证?是 - 除非我们修改这些路径名以驻留在现有目录中。然而,这是否安全可行?不能修改路径名阻止我们验证原始路径名吗?

回答这个问题,回想一下上面os.lstat()文件系统中语法正确的路径名不包含路径组件(A)包含空字节或(B)超过255个字节的长度。因此,当且仅当该路径名中的所有路径组件都有效时,FileNotFoundError路径名才有效。对于感兴趣的大多数 real-world filesystems都是如此。

这种迂腐的见解真的对我们有帮助吗?是。它减少了一次性验证完整路径名的较大问题,即仅验证该路径名中所有路径组件的较小问题。通过遵循以下算法,任何任意路径名都可以跨平台方式验证(无论该路径名是否驻留在现有目录中):

  1. 将路径名拆分为路径组件(例如,将路径名ext4拆分为列表ext4)。
  2. 对于每个这样的组件:
    1. 将保证与该组件一起存在的目录的路径名加入新的临时路径名(例如/troldskog/faren/vild)。
    2. 将该路径名传递给['', 'troldskog', 'faren', 'vild']/troldskog。如果该路径名并因此该组件无效,则此调用将保证引发异常,而不是通用os.stat()异常。为什么? 因为该路径名位于现有目录中。(循环逻辑是循环的。)
  3. 是否保证存在目录?是的,但通常只有一个:根文件系统的最顶层目录(如上所定义)。

    将驻留在任何其他目录中(因此不保证存在)的路径名传递给os.lstat()FileNotFoundError会引发竞争条件,即使该目录以前已经过测试存在。为什么?由于无法阻止外部进程在执行该测试后同时删除该目录,而之前该路径名将传递给os.stat()os.lstat()。释放心灵疯狂的狗!

    上述方法也有很大的好处:安全。(Isn' 好吗?)具体来说:

      

    通过简单地将此类路径名传递给os.stat()os.lstat()来验证来自不受信任来源的任意路径名的前置应用程序容易受到拒绝服务(DoS)攻击和其他黑帽恶作剧的影响。恶意用户可能会尝试重复验证驻留在已知过时或以其他方式变慢的文件系统上的路径名(例如,NFS Samba共享);在这种情况下,盲目地定位传入的路径名可能最终会因连接超时而失败,或者消耗更多的时间和资源而不是您承受失业的微弱能力。

    上述方法通过仅针对根文件系统的根目录验证路径名的路径组件来避免这种情况。 (如果即使 陈旧,缓慢或无法访问),您遇到的问题也比路径名验证更严重。)

    失去了什么? 很棒。让我们开始吧。 (Python 3假设。参见"什么是300的脆弱希望,leycec?")

    os.stat()

    完成。不要眯着眼看那段代码。 (它咬人。

    问题2:可能无效的路径名存在或可创建性,嗯?

    鉴于上述解决方案,测试可能无效路径名的存在或可创建性大多是微不足道的。这里的小关键是在测试传递的路径之前调用先前定义的函数

    os.lstat()

    完成完成。除非不完整。

    问题3:Windows上可能无效的路径名存在或可写性

    存在一个警告。当然有。

    官方os.access() documentation承认:

      

    注意:即使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? 表示它们会成功,I / O操作也可能失败,特别是对于可能具有超出通常POSIX权限位模型的权限语义的网络文件系统上的操作

    令人惊讶的是,Windows在这里是常见的嫌疑人。由于在NTFS文件系统上广泛使用访问控制列表(ACL),简单的POSIX权限位模型很难映射到底层的Windows现实。虽然这(可以说)不是Python的错,但它可能会引起与Windows兼容的应用程序的关注。

    如果是你,那么需要一个更强大的替代品。如果传递的路径存在,我们会尝试创建一个保证在该路径的父目录中立即删除的临时文件 - 一个更具可移植性(如果昂贵)的可创建性测试:

    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
    

    但请注意,即使也可能不够。

    感谢用户访问控制(UAC),无处不在的Windows Vista及其后续的所有迭代blatantly lie,涉及与系统目录相关的权限。当非管理员用户尝试在规范os.access()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会在实际将所有创建的文件隔离到&中时表面允许用户这样做#34;虚拟商店"在该用户的个人资料中。 (谁可能想象欺骗用户会产生有害的长期后果?)

    这很疯狂。这是Windows。

    证明

    我们敢吗?是时候试驾上述测试了。

    由于NULL是面向UNIX的文件系统上路径名中唯一禁止的字符,因此我们可以利用它来展示这个冷酷而真实的事实 - 忽略不可忽视的Windows恶作剧,坦率地说,并且同样激怒我:

    C:\Windows

    超越理智。超越痛苦。您会发现Python可移植性问题。

答案 1 :(得分:33)

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's answer 会仔细查看问题。

答案 2 :(得分:6)

使用Python 3,如何:

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

使用' x'选项我们也不必担心竞争条件。请参阅文档here

现在,如果它已经存在,这将创建一个非常短的临时文件 - 除非名称无效。如果你可以接受它,它会简化很多事情。

答案 3 :(得分:4)

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权限的here

答案 4 :(得分:4)

找到一个名为 pathvalidate 的 PyPI 模块

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

pip install pathvalidate

它内部有一个名为 sanitize 的函数,它将获取文件路径并将其转换为有效的文件路径

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

答案 5 :(得分:-2)

尝试os.path.exists这将检查路径并返回True(如果存在)和False(如果不存在)。