os.mkdir 在 Windows 上成功 shutil.rmtree 后可能会因 PermissionError 而



考虑以下用于清理目录的python函数:

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

在Windows上(实际上在Windows7和Windows10上使用python 2.7.10和3.4.4进行了测试),当同时使用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函数中(而不是在Pythonos模块中):当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)

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

您应该在循环中重试os.mkdir,并增加超时。您还需要一个用于shutil.rmtreeonerror处理程序,用于处理尝试删除非空目录,因为它包含"已删除"的文件或目录。

例如:

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)。例如,如果目录作为进程的工作目录打开,则它不会共享删除/重命名访问权限。对于常规文件,大多数程序仅共享读/执行和写入/追加访问权限。

临时文件通常使用删除/重命名访问权限共享打开,特别是如果它们以删除/重命名访问权限打开(例如,使用关闭时删除标志打开)。这是"已删除"文件仍链接但无法访问的最常见原因。另一种情况是打开一个目录来监视更改(例如,请参阅ReadDirectoryChangesW)。通常,此打开将共享删除/重命名访问权限,这就是资源管理器在此问题中的情况。


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

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

在 Windows 10 中,内核还支持 POSIX 删除语义,一旦删除句柄关闭,文件或目录就会立即取消链接(请参阅 NTAPIFileDispositionInformationEx的详细信息)。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

最新更新