X Tutup
The Wayback Machine - https://web.archive.org/web/20260304114504/https://github.com/python/cpython/pull/120883
Skip to content

gh-120882: Fix shutil.move to properly handle files opened by another process#120883

Closed
Saqibs575 wants to merge 1 commit intopython:mainfrom
Saqibs575:fix/shutil-OSError-handling
Closed

gh-120882: Fix shutil.move to properly handle files opened by another process#120883
Saqibs575 wants to merge 1 commit intopython:mainfrom
Saqibs575:fix/shutil-OSError-handling

Conversation

@Saqibs575
Copy link

@Saqibs575 Saqibs575 commented Jun 22, 2024

Bug report

Bug description:

Problem:

When attempting to move a file using shutil.move from one location to another, if the file is opened by another process (if file is opened in other python script using open function), shutil.move raises an error (WinError 32: The process cannot access the file because it is being used by another process). Despite this error, the file is still copied to the destination directory, which is unexpected behavior.

Details:

The shutil.move function is designed to handle moving files or directories between locations. However, it does not check if the file is currently open by another process before attempting to move it. This results in an error being raised, but the file is still copied to destination regardless of the error condition.

Solution:

To address this issue, I modified the shutil.move function to include an additional check for OSError with errno.EACCES (Permission Denied) in the exception handling block. This ensures that if the file cannot be moved due to being opened by another process, the operation is aborted cleanly without copying the file to maintain the integrity of the move function as well as file system.

Here is the modified part of the shutil.move function

    except OSError as exc:
        if exc.errno == errno.EACCES:
            raise  # Propagate the PermissionError
     # Existing code

Proposal:

I propose adding this error handling improvement to the shutil.move function in Python's standard library. This enhancement will make file operations more reliable in scenarios where files may be concurrently accessed by multiple processes

Steps to Reproduce:

  • Open a file (test.pdf) in any python script using open function in read mode.
  • Attempt to move the file using shutil.move to another directory.
  • Observe the WinError 32 exception and incorrect copying behavior.
  • Close the file (To avoid memory leakage).

NOTE: Avoid to use context manager here for opening file to find the error.
You can also run the following script to get the error.

import shutil

src = "C://Users//Desktop//Downloads//test.pdf" # Provide full file path of your system.
dst_dir = "D://Files" # Provide destination directory path.
f = open(src, "r")
try:
    shutil.move(src, dst_dir)
except Exception as e:
    print(e)
finally:
    f.close()

Expected Behavior:

When shutil.move encounters a situation where the source file is open in another process (WinError 32), it should raise an exception and abort the operation without performing any partial file copying to the destination.

Impact:

This issue affects users trying to use shutil.move to relocate files that are concurrently accessed by other processes. The proposed solution aims to improve the robustness and expected behavior of the function in such scenarios.

CPython versions tested on:

3.10

Operating systems tested on:

Windows

@ghost
Copy link

ghost commented Jun 22, 2024

All commit authors signed the Contributor License Agreement.
CLA signed

@bedevere-app
Copy link

bedevere-app bot commented Jun 22, 2024

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@barneygale
Copy link
Contributor

barneygale commented Jun 22, 2024

When shutil.move encounters a situation where the source file is open in another process (WinError 32), it should raise an exception and abort the operation without performing any partial file copying to the destination.

Unfortunately we can't know that ahead of time, nor can we roll back the copy if we encounter an error while deleting. Users expect that shutil.move() deletes the original file (that's what differentiates it from copy2() / copytree()) which doesn't seem compatible with your suggestion to suppress EACCES. sorry, misunderstood your proposal.

except OSError:
except OSError as exc:
if exc.errno == errno.EACCES:
raise
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise
raise

@nineteendo
Copy link
Contributor

nineteendo commented Jun 22, 2024

It doesn't raise the correct error when moving a directory on Windows (you should check that first):

======================================================================
ERROR: test_dont_move_dir_in_itself (test.test_shutil.TestMove.test_dont_move_dir_in_itself)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\a\cpython\cpython\Lib\test\test_shutil.py", line 2742, in test_dont_move_dir_in_itself
    self.assertRaises(shutil.Error, shutil.move, self.src_dir, dst)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\a\cpython\cpython\Lib\unittest\case.py", line 804, in assertRaises
    return context.handle('assertRaises', args, kwargs)
           ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\a\cpython\cpython\Lib\unittest\case.py", line 238, in handle
    callable_obj(*args, **kwargs)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "D:\a\cpython\cpython\Lib\shutil.py", line 857, in move
    os.rename(src, real_dst)
    ~~~~~~~~~^^^^^^^^^^^^^^^
PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'D:\\a\\cpython\\cpython\\build\\test_python_[833](https://github.com/python/cpython/actions/runs/9626446182/job/26552425077?pr=120883#step:6:834)6�\\tmpjagpnbs_' -> 'D:\\a\\cpython\\cpython\\build\\test_python_8336�\\tmpjagpnbs_\\bar'

Could you also add a test for the fix?

Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is wrong. You cannot simply ignore all OSErrors but EACCESS.

@bedevere-app
Copy link

bedevere-app bot commented Jun 24, 2024

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@eryksun
Copy link
Contributor

eryksun commented Jun 24, 2024

It doesn't raise the correct error when moving a directory on Windows

Note that starting with Windows 11 22H2, this test case fails with ERROR_INVALID_PARAMETER (87), which maps to C EINVAL. So it actually didn't fail for me on Windows 11 23H2. It would have failed if it were implemented to move a directory into one of its subdirectories instead of directly into itself. In Windows 11, the latter still fails with ERROR_ACCESS_DENIED (5), which maps to C EACCES.

What's changed recently is that for a rename operation, the kernel's internal I/O manager function IopOpenLinkOrRenameTarget() was updated to open the destination directory using read, write, and delete sharing, whereas it used to open it with just read and write sharing. This change is documented in [MS-FSA]: 2.1.5.15.11 FileRenameInformation where it mentions opening DestinationDirectory with "ShareAccess equal to FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE".

With the old implementation, which dates back about 30 years, the internal IopOpenLinkOrRenameTarget() call would fail with a sharing violation, i.e. Windows error ERROR_SHARING_VIOLATION (32)1 because this second open of the directory didn't share delete access. Now in Windows 11 it succeeds in this case, and the rename operation ends up failing farther along in the device stack with ERROR_INVALID_PARAMETER. This behavior is currently undocumented in the FileRenameInformation protocol. I have to wonder whether it's an oversight. If it were intentional, I'd expect the error code to be ERROR_ACCESS_DENIED.

Note that trying to rename a directory into a subdirectory of itself (e.g. "spam" -> "spam\eggs\foo") still fails with ERROR_ACCESS_DENIED, as documented where the protocol discusses "[i]f Open.File.FileType is DirectoryFile, determine whether Open.File contains open files as specified in section 2.1.4.2...". In this case, there's at least one open file in the source directory tree -- the destination directory.

Footnotes

  1. For consistency and simplicity I'm using Windows error codes instead of the actual error status codes that are used in the NT API, such as STATUS_SHARING_VIOLATION, STATUS_INVALID_PARAMETER, and STATUS_ACCESS_DENIED.

@Saqibs575 Saqibs575 closed this by deleting the head repository Sep 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

X Tutup