1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
# Copyright 2024 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
import errno
import fcntl
import logging
import os
import platform
import shutil
import sys
logger = logging.getLogger(__name__)
# Added in Python 3.12
FICLONE = getattr(fcntl, "FICLONE", 0x40049409)
# Unavailable in PyPy
SEEK_DATA = getattr(os, "SEEK_DATA", 3)
SEEK_HOLE = getattr(os, "SEEK_HOLE", 4)
def _get_chunks(src):
try:
offset_hole = 0
while True:
try:
# Find the next bit of data
offset_data = os.lseek(src, offset_hole, SEEK_DATA)
except OSError as e:
# Re-raise for unexpected errno values
if e.errno not in (errno.EINVAL, errno.ENXIO):
raise
offset_end = os.lseek(src, 0, os.SEEK_END)
if e.errno == errno.ENXIO:
# End of file
if offset_end > offset_hole:
# Hole at end of file
yield (offset_end, 0)
else:
# SEEK_DATA failed with EINVAL, return the whole file
yield (0, offset_end)
break
else:
offset_hole = os.lseek(src, offset_data, SEEK_HOLE)
yield (offset_data, offset_hole - offset_data)
except OSError:
logger.warning("_get_chunks failed unexpectedly", exc_info=sys.exc_info())
raise
def _do_copy_file_range(src, dst, offset, count):
while count > 0:
# count must fit in ssize_t
c = min(count, sys.maxsize)
written = os.copy_file_range(src, dst, c, offset, offset)
if written == 0:
# https://bugs.gentoo.org/828844
raise OSError(errno.EOPNOTSUPP, os.strerror(errno.EOPNOTSUPP))
offset += written
count -= written
def _do_sendfile(src, dst, offset, count):
os.lseek(dst, offset, os.SEEK_SET)
while count > 0:
# count must fit in ssize_t
c = min(count, sys.maxsize)
written = os.sendfile(dst, src, offset, c)
offset += written
count -= written
def _fastcopy(src, dst):
with (
open(src, "rb", buffering=0) as srcf,
open(dst, "wb", buffering=0) as dstf,
):
srcfd = srcf.fileno()
dstfd = dstf.fileno()
if platform.system() == "Linux":
try:
fcntl.ioctl(dstfd, FICLONE, srcfd)
return
except OSError:
pass
try_cfr = hasattr(os, "copy_file_range")
for offset, count in _get_chunks(srcfd):
if count == 0:
os.ftruncate(dstfd, offset)
else:
if try_cfr:
try:
_do_copy_file_range(srcfd, dstfd, offset, count)
continue
except OSError as e:
try_cfr = False
if e.errno not in (errno.EXDEV, errno.ENOSYS, errno.EOPNOTSUPP):
logger.warning(
"_do_copy_file_range failed unexpectedly",
exc_info=sys.exc_info(),
)
try:
_do_sendfile(srcfd, dstfd, offset, count)
except OSError:
logger.warning(
"_do_sendfile failed unexpectedly", exc_info=sys.exc_info()
)
raise
def copyfile(src, dst):
"""
Copy the contents (no metadata) of the file named src to a file
named dst.
If possible, copying is done within the kernel, and uses
"copy acceleration" techniques (such as reflinks). This also
supports sparse files.
@param src: path of source file
@type src: str
@param dst: path of destination file
@type dst: str
"""
try:
_fastcopy(src, dst)
except OSError:
shutil.copyfile(src, dst)
|