summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilip Withnall <philip@tecnocode.co.uk>2023-05-09 14:08:18 +0000
committerPhilip Withnall <philip@tecnocode.co.uk>2023-05-09 14:08:18 +0000
commit20964ad4ab18f5ba01f3bd1625fb9152eb8ca932 (patch)
treea1ba681bf835c2de4d4c451e0fc9a3c43348a432
parent60f38b5f75846320c7347a2d7b2ac233568bc8da (diff)
parent0e5d9fd249e357934c81a40a9ea4693aee07f5f5 (diff)
downloadglib-20964ad4ab18f5ba01f3bd1625fb9152eb8ca932.tar.gz
Merge branch '2863-copy-file-range' into 'main'
gfile: Support copy_file_range() for file copies Closes #2863 See merge request GNOME/glib!3328
-rw-r--r--gio/gfile.c163
-rw-r--r--gio/tests/file.c76
-rw-r--r--meson.build1
3 files changed, 230 insertions, 10 deletions
diff --git a/gio/gfile.c b/gio/gfile.c
index 94786c84a..31da0b05e 100644
--- a/gio/gfile.c
+++ b/gio/gfile.c
@@ -2807,6 +2807,11 @@ g_file_build_attribute_list_for_copy (GFile *file,
first = TRUE;
s = g_string_new ("");
+ /* Always query the source file size, even though we can’t set that on the
+ * destination. This is useful for the copy functions. */
+ first = FALSE;
+ g_string_append (s, G_FILE_ATTRIBUTE_STANDARD_SIZE);
+
if (attributes)
{
for (i = 0; i < attributes->n_infos; i++)
@@ -3002,6 +3007,122 @@ copy_stream_with_progress (GInputStream *in,
return res;
}
+#ifdef HAVE_COPY_FILE_RANGE
+static gboolean
+do_copy_file_range (int fd_in,
+ loff_t *off_in,
+ int fd_out,
+ loff_t *off_out,
+ size_t len,
+ size_t *bytes_transferred,
+ GError **error)
+{
+ ssize_t result;
+
+ do
+ {
+ result = copy_file_range (fd_in, off_in, fd_out, off_out, len, 0);
+
+ if (result == -1)
+ {
+ int errsv = errno;
+
+ if (errsv == EINTR)
+ {
+ continue;
+ }
+ else if (errsv == ENOSYS || errsv == EINVAL || errsv == EOPNOTSUPP || errsv == EXDEV)
+ {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+ _("Copy file range not supported"));
+ }
+ else
+ {
+ g_set_error (error, G_IO_ERROR,
+ g_io_error_from_errno (errsv),
+ _("Error splicing file: %s"),
+ g_strerror (errsv));
+ }
+
+ return FALSE;
+ }
+ } while (result == -1);
+
+ g_assert (result >= 0);
+ *bytes_transferred = result;
+
+ return TRUE;
+}
+
+static gboolean
+copy_file_range_with_progress (GInputStream *in,
+ GFileInfo *in_info,
+ GOutputStream *out,
+ GCancellable *cancellable,
+ GFileProgressCallback progress_callback,
+ gpointer progress_callback_data,
+ GError **error)
+{
+ goffset total_size, last_notified_size;
+ size_t copy_len;
+ loff_t offset_in;
+ loff_t offset_out;
+ int fd_in, fd_out;
+
+ fd_in = g_file_descriptor_based_get_fd (G_FILE_DESCRIPTOR_BASED (in));
+ fd_out = g_file_descriptor_based_get_fd (G_FILE_DESCRIPTOR_BASED (out));
+
+ g_assert (g_file_info_has_attribute (in_info, G_FILE_ATTRIBUTE_STANDARD_SIZE));
+ total_size = g_file_info_get_size (in_info);
+
+ /* Bail out if the reported size of the file is zero. It might be zero, but it
+ * might also just be a kernel file in /proc. They report their file size as
+ * zero, but then have data when you start reading. Go to the fallback code
+ * path for those. */
+ if (total_size == 0)
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+ _("Copy file range not supported"));
+ return FALSE;
+ }
+
+ offset_in = offset_out = 0;
+ copy_len = total_size;
+ last_notified_size = 0;
+
+ /* Call copy_file_range() in a loop until the whole contents are copied. For
+ * smaller files, this loop will iterate only once. For larger files, the
+ * kernel (at least, kernel 6.1.6) will return after 2GB anyway, so that gives
+ * us more loop iterations and more progress reporting. */
+ while (copy_len > 0)
+ {
+ size_t n_copied;
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error) ||
+ !do_copy_file_range (fd_in, &offset_in, fd_out, &offset_out, copy_len, &n_copied, error))
+ return FALSE;
+
+ if (n_copied == 0)
+ break;
+
+ g_assert (n_copied <= copy_len);
+ copy_len -= n_copied;
+
+ if (progress_callback)
+ {
+ progress_callback (offset_in, total_size, progress_callback_data);
+ last_notified_size = total_size;
+ }
+ }
+
+ /* Make sure we send full copied size */
+ if (progress_callback && last_notified_size != total_size)
+ progress_callback (offset_in, total_size, progress_callback_data);
+
+ return TRUE;
+}
+#endif /* HAVE_COPY_FILE_RANGE */
+
#ifdef HAVE_SPLICE
static gboolean
@@ -3042,6 +3163,7 @@ retry:
static gboolean
splice_stream_with_progress (GInputStream *in,
+ GFileInfo *in_info,
GOutputStream *out,
GCancellable *cancellable,
GFileProgressCallback progress_callback,
@@ -3085,10 +3207,8 @@ splice_stream_with_progress (GInputStream *in,
/* avoid performance impact of querying total size when it's not needed */
if (progress_callback)
{
- struct stat sbuf;
-
- if (fstat (fd_in, &sbuf) == 0)
- total_size = sbuf.st_size;
+ g_assert (g_file_info_has_attribute (in_info, G_FILE_ATTRIBUTE_STANDARD_SIZE));
+ total_size = g_file_info_get_size (in_info);
}
if (total_size == -1)
@@ -3151,6 +3271,7 @@ splice_stream_with_progress (GInputStream *in,
#ifdef __linux__
static gboolean
btrfs_reflink_with_progress (GInputStream *in,
+ GFileInfo *in_info,
GOutputStream *out,
GFileInfo *info,
GCancellable *cancellable,
@@ -3169,10 +3290,8 @@ btrfs_reflink_with_progress (GInputStream *in,
/* avoid performance impact of querying total size when it's not needed */
if (progress_callback)
{
- struct stat sbuf;
-
- if (fstat (fd_in, &sbuf) == 0)
- total_size = sbuf.st_size;
+ g_assert (g_file_info_has_attribute (in_info, G_FILE_ATTRIBUTE_STANDARD_SIZE));
+ total_size = g_file_info_get_size (in_info);
}
if (total_size == -1)
@@ -3376,7 +3495,7 @@ file_copy_fallback (GFile *source,
{
GError *reflink_err = NULL;
- if (!btrfs_reflink_with_progress (in, out, info, cancellable,
+ if (!btrfs_reflink_with_progress (in, info, out, info, cancellable,
progress_callback, progress_callback_data,
&reflink_err))
{
@@ -3398,12 +3517,36 @@ file_copy_fallback (GFile *source,
}
#endif
+#ifdef HAVE_COPY_FILE_RANGE
+ if (G_IS_FILE_DESCRIPTOR_BASED (in) && G_IS_FILE_DESCRIPTOR_BASED (out))
+ {
+ GError *copy_file_range_error = NULL;
+
+ if (copy_file_range_with_progress (in, info, out, cancellable,
+ progress_callback, progress_callback_data,
+ &copy_file_range_error))
+ {
+ ret = TRUE;
+ goto out;
+ }
+ else if (!g_error_matches (copy_file_range_error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
+ {
+ g_propagate_error (error, g_steal_pointer (&copy_file_range_error));
+ goto out;
+ }
+ else
+ {
+ g_clear_error (&copy_file_range_error);
+ }
+ }
+#endif /* HAVE_COPY_FILE_RANGE */
+
#ifdef HAVE_SPLICE
if (G_IS_FILE_DESCRIPTOR_BASED (in) && G_IS_FILE_DESCRIPTOR_BASED (out))
{
GError *splice_err = NULL;
- if (!splice_stream_with_progress (in, out, cancellable,
+ if (!splice_stream_with_progress (in, info, out, cancellable,
progress_callback, progress_callback_data,
&splice_err))
{
diff --git a/gio/tests/file.c b/gio/tests/file.c
index 754c6c326..7fbb3ef0c 100644
--- a/gio/tests/file.c
+++ b/gio/tests/file.c
@@ -2513,6 +2513,80 @@ test_copy_preserve_mode (void)
#endif
}
+typedef struct
+{
+ goffset current_num_bytes;
+ goffset total_num_bytes;
+} CopyProgressData;
+
+static void
+file_copy_progress_cb (goffset current_num_bytes,
+ goffset total_num_bytes,
+ gpointer user_data)
+{
+ CopyProgressData *prev_data = user_data;
+
+ g_assert_cmpuint (total_num_bytes, ==, prev_data->total_num_bytes);
+ g_assert_cmpuint (current_num_bytes, >=, prev_data->current_num_bytes);
+
+ /* Update it for the next callback. */
+ prev_data->current_num_bytes = current_num_bytes;
+}
+
+static void
+test_copy_progress (void)
+{
+ GFile *src_tmpfile = NULL;
+ GFile *dest_tmpfile = NULL;
+ GFileIOStream *iostream;
+ GOutputStream *ostream;
+ GError *local_error = NULL;
+ const guint8 buffer[] = { 1, 2, 3, 4, 5 };
+ CopyProgressData progress_data;
+
+ src_tmpfile = g_file_new_tmp ("tmp-copy-progressXXXXXX",
+ &iostream, &local_error);
+ g_assert_no_error (local_error);
+
+ /* Write some content to the file for testing. */
+ ostream = g_io_stream_get_output_stream (G_IO_STREAM (iostream));
+ g_output_stream_write (ostream, buffer, sizeof (buffer), NULL, &local_error);
+ g_assert_no_error (local_error);
+
+ g_io_stream_close ((GIOStream *) iostream, NULL, &local_error);
+ g_assert_no_error (local_error);
+ g_clear_object (&iostream);
+
+ /* Grab a unique destination filename. */
+ dest_tmpfile = g_file_new_tmp ("tmp-copy-progressXXXXXX",
+ &iostream, &local_error);
+ g_assert_no_error (local_error);
+ g_io_stream_close ((GIOStream *) iostream, NULL, &local_error);
+ g_assert_no_error (local_error);
+ g_clear_object (&iostream);
+
+ /* Set the progress data to an initial offset of zero. The callback will
+ * assert that progress is non-decreasing and reaches the total length of
+ * the file. */
+ progress_data.current_num_bytes = 0;
+ progress_data.total_num_bytes = sizeof (buffer);
+
+ /* Copy the file with progress reporting. */
+ g_file_copy (src_tmpfile, dest_tmpfile, G_FILE_COPY_OVERWRITE,
+ NULL, file_copy_progress_cb, &progress_data, &local_error);
+ g_assert_no_error (local_error);
+
+ g_assert_cmpuint (progress_data.current_num_bytes, ==, progress_data.total_num_bytes);
+ g_assert_cmpuint (progress_data.total_num_bytes, ==, sizeof (buffer));
+
+ /* Clean up. */
+ (void) g_file_delete (src_tmpfile, NULL, NULL);
+ (void) g_file_delete (dest_tmpfile, NULL, NULL);
+
+ g_clear_object (&src_tmpfile);
+ g_clear_object (&dest_tmpfile);
+}
+
static void
test_measure (void)
{
@@ -3844,6 +3918,7 @@ main (int argc, char *argv[])
g_test_add_func ("/file/async-delete", test_async_delete);
g_test_add_func ("/file/async-make-symlink", test_async_make_symlink);
g_test_add_func ("/file/copy-preserve-mode", test_copy_preserve_mode);
+ g_test_add_func ("/file/copy/progress", test_copy_progress);
g_test_add_func ("/file/measure", test_measure);
g_test_add_func ("/file/measure-async", test_measure_async);
g_test_add_func ("/file/load-bytes", test_load_bytes);
@@ -3869,3 +3944,4 @@ main (int argc, char *argv[])
return g_test_run ();
}
+
diff --git a/meson.build b/meson.build
index 1d75a8be5..44284fc10 100644
--- a/meson.build
+++ b/meson.build
@@ -611,6 +611,7 @@ endif
functions = [
'accept4',
'close_range',
+ 'copy_file_range',
'endmntent',
'endservent',
'epoll_create',