Skip to content

Commit aac4d03

Browse files
authored
bpo-26826: Expose copy_file_range in the os module (GH-7255)
1 parent 545a3b8 commit aac4d03

File tree

9 files changed

+363
-19
lines changed

9 files changed

+363
-19
lines changed

Doc/library/os.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,28 @@ as internal buffering of data.
707707
pass
708708

709709

710+
.. function:: copy_file_range(src, dst, count, offset_src=None, offset_dst=None)
711+
712+
Copy *count* bytes from file descriptor *src*, starting from offset
713+
*offset_src*, to file descriptor *dst*, starting from offset *offset_dst*.
714+
If *offset_src* is None, then *src* is read from the current position;
715+
respectively for *offset_dst*. The files pointed by *src* and *dst*
716+
must reside in the same filesystem, otherwise an :exc:`OSError` is
717+
raised with :attr:`~OSError.errno` set to :data:`errno.EXDEV`.
718+
719+
This copy is done without the additional cost of transferring data
720+
from the kernel to user space and then back into the kernel. Additionally,
721+
some filesystems could implement extra optimizations. The copy is done as if
722+
both files are opened as binary.
723+
724+
The return value is the amount of bytes copied. This could be less than the
725+
amount requested.
726+
727+
.. availability:: Linux kernel >= 4.5 or glibc >= 2.27.
728+
729+
.. versionadded:: 3.8
730+
731+
710732
.. function:: device_encoding(fd)
711733

712734
Return a string describing the encoding of the device associated with *fd*

Lib/test/test_os.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,89 @@ def test_symlink_keywords(self):
231231
except (NotImplementedError, OSError):
232232
pass # No OS support or unprivileged user
233233

234+
@unittest.skipUnless(hasattr(os, 'copy_file_range'), 'test needs os.copy_file_range()')
235+
def test_copy_file_range_invalid_values(self):
236+
with self.assertRaises(ValueError):
237+
os.copy_file_range(0, 1, -10)
238+
239+
@unittest.skipUnless(hasattr(os, 'copy_file_range'), 'test needs os.copy_file_range()')
240+
def test_copy_file_range(self):
241+
TESTFN2 = support.TESTFN + ".3"
242+
data = b'0123456789'
243+
244+
create_file(support.TESTFN, data)
245+
self.addCleanup(support.unlink, support.TESTFN)
246+
247+
in_file = open(support.TESTFN, 'rb')
248+
self.addCleanup(in_file.close)
249+
in_fd = in_file.fileno()
250+
251+
out_file = open(TESTFN2, 'w+b')
252+
self.addCleanup(support.unlink, TESTFN2)
253+
self.addCleanup(out_file.close)
254+
out_fd = out_file.fileno()
255+
256+
try:
257+
i = os.copy_file_range(in_fd, out_fd, 5)
258+
except OSError as e:
259+
# Handle the case in which Python was compiled
260+
# in a system with the syscall but without support
261+
# in the kernel.
262+
if e.errno != errno.ENOSYS:
263+
raise
264+
self.skipTest(e)
265+
else:
266+
# The number of copied bytes can be less than
267+
# the number of bytes originally requested.
268+
self.assertIn(i, range(0, 6));
269+
270+
with open(TESTFN2, 'rb') as in_file:
271+
self.assertEqual(in_file.read(), data[:i])
272+
273+
@unittest.skipUnless(hasattr(os, 'copy_file_range'), 'test needs os.copy_file_range()')
274+
def test_copy_file_range_offset(self):
275+
TESTFN4 = support.TESTFN + ".4"
276+
data = b'0123456789'
277+
bytes_to_copy = 6
278+
in_skip = 3
279+
out_seek = 5
280+
281+
create_file(support.TESTFN, data)
282+
self.addCleanup(support.unlink, support.TESTFN)
283+
284+
in_file = open(support.TESTFN, 'rb')
285+
self.addCleanup(in_file.close)
286+
in_fd = in_file.fileno()
287+
288+
out_file = open(TESTFN4, 'w+b')
289+
self.addCleanup(support.unlink, TESTFN4)
290+
self.addCleanup(out_file.close)
291+
out_fd = out_file.fileno()
292+
293+
try:
294+
i = os.copy_file_range(in_fd, out_fd, bytes_to_copy,
295+
offset_src=in_skip,
296+
offset_dst=out_seek)
297+
except OSError as e:
298+
# Handle the case in which Python was compiled
299+
# in a system with the syscall but without support
300+
# in the kernel.
301+
if e.errno != errno.ENOSYS:
302+
raise
303+
self.skipTest(e)
304+
else:
305+
# The number of copied bytes can be less than
306+
# the number of bytes originally requested.
307+
self.assertIn(i, range(0, bytes_to_copy+1));
308+
309+
with open(TESTFN4, 'rb') as in_file:
310+
read = in_file.read()
311+
# seeked bytes (5) are zero'ed
312+
self.assertEqual(read[:out_seek], b'\x00'*out_seek)
313+
# 012 are skipped (in_skip)
314+
# 345678 are copied in the file (in_skip + bytes_to_copy)
315+
self.assertEqual(read[out_seek:],
316+
data[in_skip:in_skip+i])
234317

235318
# Test attributes on return values from os.*stat* family.
236319
class StatAttributeTests(unittest.TestCase):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Expose :func:`copy_file_range` as a low level API in the :mod:`os` module.

Modules/clinic/posixmodule.c.h

Lines changed: 107 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/posixmodule.c

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ corresponding Unix manual entries for more information on calls.");
117117
#include <sched.h>
118118
#endif
119119

120+
#ifdef HAVE_COPY_FILE_RANGE
121+
#include <unistd.h>
122+
#endif
123+
120124
#if !defined(CPU_ALLOC) && defined(HAVE_SCHED_SETAFFINITY)
121125
#undef HAVE_SCHED_SETAFFINITY
122126
#endif
@@ -9455,8 +9459,74 @@ os_pwritev_impl(PyObject *module, int fd, PyObject *buffers, Py_off_t offset,
94559459
}
94569460
#endif /* HAVE_PWRITEV */
94579461

9462+
#ifdef HAVE_COPY_FILE_RANGE
9463+
/*[clinic input]
9464+
9465+
os.copy_file_range
9466+
src: int
9467+
Source file descriptor.
9468+
dst: int
9469+
Destination file descriptor.
9470+
count: Py_ssize_t
9471+
Number of bytes to copy.
9472+
offset_src: object = None
9473+
Starting offset in src.
9474+
offset_dst: object = None
9475+
Starting offset in dst.
9476+
9477+
Copy count bytes from one file descriptor to another.
9478+
9479+
If offset_src is None, then src is read from the current position;
9480+
respectively for offset_dst.
9481+
[clinic start generated code]*/
9482+
9483+
static PyObject *
9484+
os_copy_file_range_impl(PyObject *module, int src, int dst, Py_ssize_t count,
9485+
PyObject *offset_src, PyObject *offset_dst)
9486+
/*[clinic end generated code: output=1a91713a1d99fc7a input=42fdce72681b25a9]*/
9487+
{
9488+
off_t offset_src_val, offset_dst_val;
9489+
off_t *p_offset_src = NULL;
9490+
off_t *p_offset_dst = NULL;
9491+
Py_ssize_t ret;
9492+
int async_err = 0;
9493+
/* The flags argument is provided to allow
9494+
* for future extensions and currently must be to 0. */
9495+
int flags = 0;
9496+
9497+
9498+
if (count < 0) {
9499+
PyErr_SetString(PyExc_ValueError, "negative value for 'count' not allowed");
9500+
return NULL;
9501+
}
9502+
9503+
if (offset_src != Py_None) {
9504+
if (!Py_off_t_converter(offset_src, &offset_src_val)) {
9505+
return NULL;
9506+
}
9507+
p_offset_src = &offset_src_val;
9508+
}
94589509

9510+
if (offset_dst != Py_None) {
9511+
if (!Py_off_t_converter(offset_dst, &offset_dst_val)) {
9512+
return NULL;
9513+
}
9514+
p_offset_dst = &offset_dst_val;
9515+
}
94599516

9517+
do {
9518+
Py_BEGIN_ALLOW_THREADS
9519+
ret = copy_file_range(src, p_offset_src, dst, p_offset_dst, count, flags);
9520+
Py_END_ALLOW_THREADS
9521+
} while (ret < 0 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
9522+
9523+
if (ret < 0) {
9524+
return (!async_err) ? posix_error() : NULL;
9525+
}
9526+
9527+
return PyLong_FromSsize_t(ret);
9528+
}
9529+
#endif /* HAVE_COPY_FILE_RANGE*/
94609530

94619531
#ifdef HAVE_MKFIFO
94629532
/*[clinic input]
@@ -13432,6 +13502,7 @@ static PyMethodDef posix_methods[] = {
1343213502
OS_POSIX_SPAWN_METHODDEF
1343313503
OS_POSIX_SPAWNP_METHODDEF
1343413504
OS_READLINK_METHODDEF
13505+
OS_COPY_FILE_RANGE_METHODDEF
1343513506
OS_RENAME_METHODDEF
1343613507
OS_REPLACE_METHODDEF
1343713508
OS_RMDIR_METHODDEF

0 commit comments

Comments
 (0)