在Windows上成功关闭shutil.rmtree后,os.mkdir可能会因PermissionError而失败

2024-10-02 02:31:14 发布

您现在位置:Python中文网/ 问答频道 /正文

考虑下面的Python函数来清理目录:

def cleanDir(path):
  shutil.rmtree(path)
  os.mkdir(path)

在Windows上(使用python 2.7.10和3.4.4在Windows7和Windows10上进行了实际测试),当使用Windows资源管理器同时导航到相应的目录时(或仅在左侧树窗格中导航到父文件夹时),可能会引发以下异常:

Traceback (most recent call last):
  ...
  File "cleanDir.py", line ..., in cleanDir
    os.mkdir(path)
PermissionError: [WinError 5] Access is denied: 'testFolder'

这个问题已在本报告中报告。但是没有进行进一步的分析,并且给出的使用睡眠的解决方案并不令人满意。根据Eryk下面的评论,在当前python版本(即python 3.8)之前,同样的行为也是可以预期的

注意shutil.rmtree毫无例外地返回。但立即尝试再次创建目录可能会失败。(重试在大多数情况下都是成功的,请参阅下面的测试完整代码。)请注意,您需要在Windows资源管理器的测试文件夹(左侧和右侧)中单击以强制解决问题

问题似乎出在Windows文件系统API函数中(而不是Python os模块中):当Windows资源管理器对相应的文件夹具有句柄时,删除的文件夹似乎不会立即“转发”到所有函数

import os, shutil
import time

def populateFolder(path):
  if os.path.exists(path):
    with open(os.path.join(path,'somefile.txt'), 'w') as f:
      f.write('test')
  #subfolderpath = os.path.join(path,'subfolder')
  #os.mkdir(subfolderpath)
  #with open(os.path.join(subfolderpath,'anotherfile.txt'), 'w') as f2:
  #  f2.write('test')

def cleanDir(path):
  shutil.rmtree(path)
  os.mkdir(path)


def cleanDir_safe(path):
  shutil.rmtree(path)

  try:
    #time.sleep(0.005) # makes first try of os.mkdir successful
    os.mkdir(path)
  except Exception as e:
    print('os.mkdir failed: %s' % e)
    time.sleep(0.01)
    os.mkdir(path)

  assert os.path.exists(path)


FOLDER_PATH = 'testFolder'
if os.path.exists(FOLDER_PATH):
  cleanDir(FOLDER_PATH)
else:
  os.mkdir(FOLDER_PATH)

loopCnt = 0
while True:
  populateFolder(FOLDER_PATH)
  #cleanDir(FOLDER_PATH)
  cleanDir_safe(FOLDER_PATH)
  time.sleep(0.01)
  loopCnt += 1
  if loopCnt % 100 == 0:
    print(loopCnt)

Tags: path函数目录文件夹timeoswindowsdef
2条回答

资源管理器具有共享删除/重命名访问权限的目录的打开句柄。这允许rmdir成功,而正常情况下,open不会共享删除/重命名访问,并且rmdir会因共享冲突而失败(32)。但是,即使rmdir成功,在资源管理器关闭其句柄之前,目录实际上不会解除链接。它正在监视目录的更改,因此会收到目录已被删除的通知,但即使它立即关闭其句柄,脚本的os.mkdir调用也会出现竞争条件

您应该在循环中重试os.mkdir,超时时间会增加。您还需要一个onerror处理程序来处理shutil.rmtree试图删除的目录不是空的,因为它包含“已删除”的文件或目录

例如:

import os
import time
import errno
import shutil

def onerror(function, path, exc_info):
    # Handle ENOTEMPTY for rmdir
    if (function is os.rmdir
          and issubclass(exc_info[0], OSError)
          and exc_info[1].errno == errno.ENOTEMPTY):
        timeout = 0.001
        while timeout < 2:
            if not os.listdir(path):
                return os.rmdir(path)
            time.sleep(timeout)
            timeout *= 2
    raise

def clean_dir_safe(path):
    shutil.rmtree(path, onerror=onerror)
    # rmtree didn't fail, but path may still be linked if there is or was
    # a handle that shares delete access. Assume the owner of the handle
    # is watching for changes and will close it ASAP. So retry creating
    # the directory by using a loop with an increasing timeout.
    timeout = 0.001
    while True:
        try:
            return os.mkdir(path)
        except PermissionError as e:
            # Getting access denied (5) when trying to create a file or
            # directory means either the caller lacks access to the
            # parent directory or that a file or directory with that
            # name exists but is in the deleted state. Handle both cases
            # the same way. Otherwise, re-raise the exception for other
            # permission errors, such as a sharing violation (32).
            if e.winerror != 5 or timeout >= 2:
                raise
            time.sleep(timeout)
            timeout *= 2

讨论

在常见情况下,可以“避免”此问题,因为现有打开不共享删除/重命名访问权限。在这种情况下,尝试删除文件或目录失败,共享冲突(winerror 32)。例如,如果某个目录作为进程的工作目录打开,则它不共享删除/重命名访问权限。对于常规文件,大多数程序只共享读/执行和写/附加访问

临时文件通常使用“删除/重命名访问共享”打开,尤其是使用“删除/重命名”访问打开临时文件时(例如,使用“关闭时删除”标志打开)。这是导致“已删除”文件仍然链接但无法访问的最常见原因。另一种情况是打开目录以监视更改(例如,请参见^{})。通常,此打开将共享删除/重命名访问权限,这是本问题中Explorer的情况


对于Unix开发人员来说,声明文件被删除而没有取消链接可能听起来很奇怪(至少可以这么说)。在Windows中,删除文件(或目录)只是在其文件控制块(FCB)上设置删除配置。当文件系统清除文件的最后一个内核文件对象引用时,设置了delete处置集的文件将自动取消链接。文件对象通常由CreateFileW创建,它返回对象的句柄。当文件对象的最后一个句柄关闭时,会触发文件对象的清理。由于子进程中的句柄继承或显式DuplicateHandle调用,文件对象可能存在多个句柄引用

重申一下,一个文件或目录可能被多个内核文件对象引用,每个内核文件对象可能被多个句柄引用。通常,使用经典的Windows删除语义,在文件解除链接之前,必须关闭所有句柄。此外,设置delete处置并不一定是最终的。如果任何打开的句柄具有删除/重命名访问权限,则实际上可以通过清除删除处置(例如,请参见^{}:FileDispositionInfo)来恢复对文件的访问权限

在Windows 10中,内核还支持POSIX delete语义,即一旦删除句柄关闭,文件或目录就会立即取消链接(请参阅NTAPI^{}的详细信息)。NTFS已更新为支持POSIX删除语义。最近,WINAPIDeleteFileW(即Pythonos.remove)已切换到在文件系统支持的情况下使用它,但RemoveDirectoryW(即Pythonos.rmdir)仍限于经典的Windows删除

对于NTFS来说,实现POSIX语义相对容易。它只需设置删除配置并将文件重命名为NTFS保留目录“\$Extend\$Deleted”,其名称基于其文件ID。实际上,该文件似乎已取消链接,同时继续允许现有文件对象访问该文件。与传统删除相比,一个显著的区别是原始名称丢失,因此具有删除/重命名访问权限的现有句柄无法取消删除处置

我重命名文件夹并删除“新建”文件夹,然后创建(预期)文件夹

## Rename & Delete 'old' folder
if os.path.exists(file_path):
    sTmp = "fldr_" + datetime.now().strftime("%Y%m%d%H%M%S") # new folder name
    os.rename(file_path, sTmp)  # rename folder
    shutil.rmtree(sTmp)  # delete folder

## Create new folder
os.makedirs(file_path)  # make new folder

相关问题 更多 >

    热门问题