1"""
2Move a file in the safest way possible::
3
4 >>> from django.core.files.move import file_move_safe
5 >>> file_move_safe("/tmp/old_file", "/tmp/new_file")
6"""
7
8import os
9from shutil import copymode, copystat
10
11from django.core.files import locks
12
13__all__ = ["file_move_safe"]
14
15
16def file_move_safe(
17 old_file_name, new_file_name, chunk_size=1024 * 64, allow_overwrite=False
18):
19 """
20 Move a file from one location to another in the safest way possible.
21
22 First, try ``os.rename``, which is simple but will break across filesystems.
23 If that fails, stream manually from one file to another in pure Python.
24
25 If the destination file exists and ``allow_overwrite`` is ``False``, raise
26 ``FileExistsError``.
27 """
28 # There's no reason to move if we don't have to.
29 try:
30 if os.path.samefile(old_file_name, new_file_name):
31 return
32 except OSError:
33 pass
34
35 if not allow_overwrite and os.access(new_file_name, os.F_OK):
36 raise FileExistsError(
37 f"Destination file {new_file_name} exists and allow_overwrite is False."
38 )
39
40 try:
41 os.rename(old_file_name, new_file_name)
42 return
43 except OSError:
44 # OSError happens with os.rename() if moving to another filesystem or
45 # when moving opened files on certain operating systems.
46 pass
47
48 # first open the old file, so that it won't go away
49 with open(old_file_name, "rb") as old_file:
50 # now open the new file, not forgetting allow_overwrite
51 fd = os.open(
52 new_file_name,
53 (
54 os.O_WRONLY
55 | os.O_CREAT
56 | getattr(os, "O_BINARY", 0)
57 | (os.O_EXCL if not allow_overwrite else 0)
58 ),
59 )
60 try:
61 locks.lock(fd, locks.LOCK_EX)
62 current_chunk = None
63 while current_chunk != b"":
64 current_chunk = old_file.read(chunk_size)
65 os.write(fd, current_chunk)
66 finally:
67 locks.unlock(fd)
68 os.close(fd)
69
70 try:
71 copystat(old_file_name, new_file_name)
72 except PermissionError:
73 # Certain filesystems (e.g. CIFS) fail to copy the file's metadata if
74 # the type of the destination filesystem isn't the same as the source
75 # filesystem. This also happens with some SELinux-enabled systems.
76 # Ignore that, but try to set basic permissions.
77 try:
78 copymode(old_file_name, new_file_name)
79 except PermissionError:
80 pass
81
82 try:
83 os.remove(old_file_name)
84 except PermissionError as e:
85 # Certain operating systems (Cygwin and Windows)
86 # fail when deleting opened files, ignore it. (For the
87 # systems where this happens, temporary files will be auto-deleted
88 # on close anyway.)
89 if getattr(e, "winerror", 0) != 32:
90 raise