diff --git a/.gitignore b/.gitignore index 2a64eee33..9ccd8b058 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,11 @@ debian/files *.debhelper obj-x86_64-linux-gnu + +# Ignore additional dev environment files +.vscode/ +.clang-format +.clangd + +#Ignore .md files with "MY_" prefix +MY_*.md \ No newline at end of file diff --git a/libnemo-private/meson.build b/libnemo-private/meson.build index 47ac9f868..c2697c07c 100644 --- a/libnemo-private/meson.build +++ b/libnemo-private/meson.build @@ -79,6 +79,7 @@ nemo_private_sources = [ 'nemo-vfs-file.c', 'nemo-widget-action.c', 'nemo-widget-menu-item.c', + 'nemo-gfile.c' ] nemo_private_deps = [ diff --git a/libnemo-private/nemo-file-operations.c b/libnemo-private/nemo-file-operations.c index eb4d8a9bf..97b369cc7 100644 --- a/libnemo-private/nemo-file-operations.c +++ b/libnemo-private/nemo-file-operations.c @@ -36,6 +36,9 @@ #include #include #include +#include +#include +#include #include "nemo-file-operations.h" @@ -70,6 +73,7 @@ #include "nemo-file-undo-operations.h" #include "nemo-file-undo-manager.h" #include "nemo-job-queue.h" +#include "nemo-gfile.h" /* TODO: TESTING!!! */ @@ -186,6 +190,13 @@ typedef struct { int last_reported_files_left; } TransferInfo; +typedef struct { + CopyMoveJob *job; + SourceInfo *source_info; + TransferInfo *transfer_info; + goffset last_size; +} ProgressData; + #define SECONDS_NEEDED_FOR_RELIABLE_TRANSFER_RATE 8 #define US_PER_MS 1000 #define PROGRESS_UPDATE_THRESHOLD 250 @@ -4174,13 +4185,6 @@ remove_target_recursively (CommonJob *job, } -typedef struct { - CopyMoveJob *job; - goffset last_size; - SourceInfo *source_info; - TransferInfo *transfer_info; -} ProgressData; - static void copy_file_progress_callback (goffset current_num_bytes, goffset total_num_bytes, @@ -4397,6 +4401,160 @@ get_target_file_for_display_name (GFile *dir, return dest; } +/* Determine if a given source file is a regular file e.g. no link and + * if the target device is a consumer grade USB block device */ +static gboolean +is_regular_gfile_and_dest_is_consumer_usb_blk_device (GFile *src, GFile *dest) +{ + /* 1. Check if src is a regular file */ + GFileType type = + g_file_query_file_type (src, G_FILE_QUERY_INFO_NONE, NULL); + if (type != G_FILE_TYPE_REGULAR) { + return FALSE; + } + + /* 2. Get the path of dest */ + gchar *dest_path = g_file_get_path (dest); + if (!dest_path) { + return FALSE; + } + + /* 3. Find the longest matching mount entry for dest_path */ + FILE *mounts = setmntent ("/proc/mounts", "r"); + if (!mounts) { + g_free (dest_path); + return FALSE; + } + + struct mntent *ent; + gchar *best_mnt_fsname = NULL; + size_t best_mnt_len = 0; + + while ((ent = getmntent (mounts)) != NULL) { + size_t mnt_len = strlen (ent->mnt_dir); + + /* mount point must be a prefix of dest_path and either + * be the root "/" or be followed by "/" or end of string + * to avoid partial directory name matches */ + if (strncmp (dest_path, ent->mnt_dir, mnt_len) == 0 && + (dest_path[mnt_len] == '/' || dest_path[mnt_len] == '\0') && + mnt_len > best_mnt_len) { + struct stat st; + if (stat (ent->mnt_fsname, &st) == 0 && + S_ISBLK (st.st_mode)) { + g_free (best_mnt_fsname); + best_mnt_fsname = g_strdup (ent->mnt_fsname); + best_mnt_len = mnt_len; + } + } + } + endmntent (mounts); + g_free (dest_path); + + if (!best_mnt_fsname) { + return FALSE; + } + + /* 4. Get major:minor of the block device */ + struct stat dev_st; + if (stat (best_mnt_fsname, &dev_st) != 0) { + g_free (best_mnt_fsname); + return FALSE; + } + g_free (best_mnt_fsname); + + unsigned int maj = major (dev_st.st_rdev); + unsigned int min = minor (dev_st.st_rdev); + + /* 5. Resolve the sysfs path for this device via /sys/dev/block/maj:min */ + gchar *sys_block_link = + g_strdup_printf ("/sys/dev/block/%u:%u", maj, min); + char resolved_buf[PATH_MAX]; + gchar *resolved = realpath (sys_block_link, resolved_buf); + g_free (sys_block_link); + + if (!resolved) { + return FALSE; + } + + /* resolved now points to resolved_buf — no need to free */ + gchar *disk_sys_path; + + /* 6. If this is a partition, go up one level to the whole disk */ + gchar *partition_file = + g_build_filename (resolved_buf, "partition", NULL); + if (g_file_test (partition_file, G_FILE_TEST_EXISTS)) { + disk_sys_path = g_path_get_dirname (resolved_buf); + } else { + disk_sys_path = g_strdup (resolved_buf); + } + g_free (partition_file); + + /* 7. Check removable flag */ + gchar *removable_path = + g_build_filename (disk_sys_path, "removable", NULL); + gchar *removable_str = NULL; + gboolean is_removable = FALSE; + + if (g_file_get_contents (removable_path, &removable_str, NULL, NULL)) { + is_removable = (removable_str[0] == '1'); + g_free (removable_str); + } + g_free (removable_path); + + if (!is_removable) { + g_free (disk_sys_path); + return FALSE; + } + + /* 8. Walk up the sysfs tree looking for a 'subsystem' symlink + * that resolves to 'usb' */ + gboolean is_usb = FALSE; + gchar *curr_path = g_strdup (disk_sys_path); + g_free (disk_sys_path); + + while (curr_path != NULL && strlen (curr_path) > strlen ("/sys")) { + gchar *subsystem_path = + g_build_filename (curr_path, "subsystem", NULL); + char sub_resolved_buf[PATH_MAX]; + gchar *sub_resolved = + realpath (subsystem_path, sub_resolved_buf); + g_free (subsystem_path); + + if (sub_resolved) { + gchar *subsystem_name = + g_path_get_basename (sub_resolved_buf); + + if (g_str_equal (subsystem_name, "usb")) { + is_usb = TRUE; + g_free (subsystem_name); + g_free (curr_path); + curr_path = NULL; + break; + } + g_free (subsystem_name); + } + + gchar *parent_dir = g_path_get_dirname (curr_path); + + if (g_str_equal (parent_dir, curr_path)) { + g_free (parent_dir); + g_free (curr_path); + curr_path = NULL; + break; + } + + g_free (curr_path); + curr_path = parent_dir; + } + + if (curr_path) { + g_free (curr_path); + } + + return is_usb; +} + /* Debuting files is non-NULL only for toplevel items */ static void copy_move_file (CopyMoveJob *copy_job, @@ -4549,20 +4707,45 @@ copy_move_file (CopyMoveJob *copy_job, pdata.source_info = source_info; pdata.transfer_info = transfer_info; - if (copy_job->is_move) { - res = g_file_move (src, dest, - flags, - job->cancellable, - copy_file_progress_callback, - &pdata, - &error); + if (is_regular_gfile_and_dest_is_consumer_usb_blk_device (src, dest)) { + if (copy_job->is_move) { + res = nemo_g_file_move_to_blk_sync ( + src, + dest, + flags, + job->cancellable, + copy_file_progress_callback, + &pdata, + &error); + } else { + res = nemo_g_file_copy_to_blk_sync ( + src, + dest, + flags, + job->cancellable, + copy_file_progress_callback, + &pdata, + &error); + } } else { - res = g_file_copy (src, dest, - flags, - job->cancellable, - copy_file_progress_callback, - &pdata, - &error); + // Use GIO's copy and move file operations (uses streams/pipes with splice) + if (copy_job->is_move) { + res = g_file_move (src, + dest, + flags, + job->cancellable, + copy_file_progress_callback, + &pdata, + &error); + } else { + res = g_file_copy (src, + dest, + flags, + job->cancellable, + copy_file_progress_callback, + &pdata, + &error); + } } if (res) { diff --git a/libnemo-private/nemo-file-watcher.c b/libnemo-private/nemo-file-watcher.c new file mode 100644 index 000000000..5d3a5a9bd --- /dev/null +++ b/libnemo-private/nemo-file-watcher.c @@ -0,0 +1,326 @@ +// this is not working - just for illustration + +#include "nemo-file-watcher.h" + +#include +#include +#include +#include +#include +#include + +#define WATCHER_POLL_INTERVAL_MS 500 + +static gboolean +watcher_read_inotify_events (FileWatcher *watcher, + gboolean *found_tempfile, + gboolean *found_rename) +{ + char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event)))); + ssize_t len; + char *ptr; + + *found_tempfile = FALSE; + *found_rename = FALSE; + + len = read (watcher->inotify_fd, buf, sizeof (buf)); + if (len <= 0) + return TRUE; + + ptr = buf; + while (ptr < buf + len) { + struct inotify_event *event = (struct inotify_event *) ptr; + + if (event->len > 0) { + if ((event->mask & IN_CREATE) && + g_str_has_prefix (event->name, ".goutputstream-")) { + if (watcher->temp_file == NULL) { + watcher->temp_file = g_file_get_child (watcher->dest_parent, + event->name); + *found_tempfile = TRUE; + } + } + + if ((event->mask & IN_MOVED_TO) && + strcmp (event->name, watcher->dest_basename) == 0) { + *found_rename = TRUE; + } + } + + ptr += sizeof (struct inotify_event) + event->len; + } + + return TRUE; +} + +static gpointer +file_watcher_thread (gpointer user_data) +{ + FileWatcher *watcher = user_data; + WatcherState state = WATCHER_STATE_WAITING_FOR_TEMPFILE; + struct pollfd pfd; + + /* arm inotify first — must happen before copy thread calls g_file_copy */ + char *parent_path = g_file_get_path (watcher->dest_parent); + watcher->watch_descriptor = inotify_add_watch (watcher->inotify_fd, + parent_path, + IN_CREATE | IN_MOVED_TO); + g_free (parent_path); + + /* signal the copy thread it can proceed */ + g_mutex_lock (&watcher->ready_mutex); + watcher->watcher_ready = TRUE; + g_cond_signal (&watcher->ready_cond); + g_mutex_unlock (&watcher->ready_mutex); + + nemo_progress_info_start (watcher->progress); + + pfd.fd = watcher->inotify_fd; + pfd.events = POLLIN; + + while (state != WATCHER_STATE_DONE) { + + poll (&pfd, 1, WATCHER_POLL_INTERVAL_MS); + + if (g_cancellable_is_cancelled (watcher->cancellable)) { + state = WATCHER_STATE_DONE; + break; + } + + gboolean found_tempfile = FALSE; + gboolean found_rename = FALSE; + + if (pfd.revents & POLLIN) { + watcher_read_inotify_events (watcher, &found_tempfile, &found_rename); + } + + g_mutex_lock (&watcher->mutex); + gboolean copy_done = watcher->copy_done; + g_mutex_unlock (&watcher->mutex); + + switch (state) { + + case WATCHER_STATE_WAITING_FOR_TEMPFILE: + if (found_tempfile) { + state = WATCHER_STATE_MONITORING_TEMPFILE; + /* fall through to stat immediately */ + } else if (copy_done) { + /* very small file — copy finished before temp file was seen */ + state = WATCHER_STATE_FLUSH_WAIT; + break; + } else { + break; + } + /* fall through */ + + case WATCHER_STATE_MONITORING_TEMPFILE: { + GError *error = NULL; + GFileInfo *info = g_file_query_info (watcher->temp_file, + G_FILE_ATTRIBUTE_STANDARD_SIZE, + G_FILE_QUERY_INFO_NONE, + watcher->cancellable, + &error); + if (info) { + goffset actual_size = g_file_info_get_size (info); + g_object_unref (info); + + g_mutex_lock (&watcher->mutex); + watcher->error = FALSE; + g_mutex_unlock (&watcher->mutex); + + if (watcher->expected_total_bytes > 0) { + nemo_progress_info_set_progress (watcher->progress, + actual_size, + watcher->expected_total_bytes); + } + } else { + g_mutex_lock (&watcher->mutex); + watcher->error = TRUE; + g_mutex_unlock (&watcher->mutex); + + if (error) g_error_free (error); + } + + if (found_rename || copy_done) { + g_clear_object (&watcher->temp_file); + state = WATCHER_STATE_FLUSH_WAIT; + } + break; + } + + case WATCHER_STATE_FLUSH_WAIT: { + GError *error = NULL; + GFileInfo *info = g_file_query_info (watcher->dest, + G_FILE_ATTRIBUTE_STANDARD_SIZE, + G_FILE_QUERY_INFO_NONE, + watcher->cancellable, + &error); + if (info) { + goffset actual_size = g_file_info_get_size (info); + g_object_unref (info); + + g_mutex_lock (&watcher->mutex); + watcher->error = FALSE; + g_mutex_unlock (&watcher->mutex); + + if (actual_size >= watcher->expected_total_bytes) { + nemo_progress_info_set_progress (watcher->progress, 1.0, 1.0); + state = WATCHER_STATE_DONE; + } else { + nemo_progress_info_take_status (watcher->progress, + g_strdup (_("Flushing to device..."))); + if (watcher->expected_total_bytes > 0) { + nemo_progress_info_set_progress (watcher->progress, + actual_size, + watcher->expected_total_bytes); + } + } + } else { + g_mutex_lock (&watcher->mutex); + watcher->error = TRUE; + gboolean fallback_has_data = (watcher->total_num_bytes > 0); + goffset fallback_current = watcher->current_num_bytes; + goffset fallback_total = watcher->total_num_bytes; + g_mutex_unlock (&watcher->mutex); + + if (error) g_error_free (error); + + if (fallback_has_data) { + nemo_progress_info_set_progress (watcher->progress, + fallback_current, + fallback_total); + } + + if (copy_done) { + state = WATCHER_STATE_DONE; + } + } + break; + } + + case WATCHER_STATE_DONE: + break; + } + } + + *watcher->progress_done = TRUE; + nemo_progress_info_finish (watcher->progress); + + g_mutex_lock (&watcher->done_mutex); + watcher->watcher_done = TRUE; + g_cond_signal (&watcher->done_cond); + g_mutex_unlock (&watcher->done_mutex); + + return NULL; +} + +FileWatcher * +file_watcher_new (GFile *dest, + goffset expected_total_bytes, + GCancellable *cancellable, + NemoProgressInfo *progress, + gboolean *progress_done) +{ + FileWatcher *watcher = g_new0 (FileWatcher, 1); + + g_mutex_init (&watcher->mutex); + g_mutex_init (&watcher->ready_mutex); + g_mutex_init (&watcher->done_mutex); + g_cond_init (&watcher->ready_cond); + g_cond_init (&watcher->done_cond); + + watcher->dest = g_object_ref (dest); + watcher->dest_parent = g_file_get_parent (dest); + watcher->dest_basename = g_file_get_basename (dest); + watcher->expected_total_bytes = expected_total_bytes; + watcher->cancellable = g_object_ref (cancellable); + watcher->progress = g_object_ref (progress); + watcher->progress_done = progress_done; + watcher->temp_file = NULL; + + watcher->inotify_fd = inotify_init1 (IN_NONBLOCK); + watcher->watch_descriptor = -1; + + watcher->watcher_ready = FALSE; + watcher->watcher_done = FALSE; + watcher->copy_done = FALSE; + watcher->error = FALSE; + watcher->current_num_bytes = 0; + watcher->total_num_bytes = 0; + + return watcher; +} + +void +file_watcher_start (FileWatcher *watcher) +{ + g_return_if_fail (watcher != NULL); + + watcher->thread = g_thread_new ("file-watcher", + file_watcher_thread, + watcher); +} + +void +file_watcher_wait_ready (FileWatcher *watcher) +{ + g_return_if_fail (watcher != NULL); + + g_mutex_lock (&watcher->ready_mutex); + while (!watcher->watcher_ready) { + g_cond_wait (&watcher->ready_cond, &watcher->ready_mutex); + } + g_mutex_unlock (&watcher->ready_mutex); +} + +void +file_watcher_set_copy_done (FileWatcher *watcher) +{ + g_return_if_fail (watcher != NULL); + + g_mutex_lock (&watcher->mutex); + watcher->copy_done = TRUE; + g_mutex_unlock (&watcher->mutex); +} + +void +file_watcher_wait (FileWatcher *watcher) +{ + g_return_if_fail (watcher != NULL); + + g_mutex_lock (&watcher->done_mutex); + while (!watcher->watcher_done) { + g_cond_wait (&watcher->done_cond, &watcher->done_mutex); + } + g_mutex_unlock (&watcher->done_mutex); + + g_thread_join (watcher->thread); + watcher->thread = NULL; +} + +void +file_watcher_free (FileWatcher *watcher) +{ + g_return_if_fail (watcher != NULL); + + if (watcher->watch_descriptor != -1) { + inotify_rm_watch (watcher->inotify_fd, watcher->watch_descriptor); + } + close (watcher->inotify_fd); + + g_clear_object (&watcher->temp_file); + g_clear_object (&watcher->dest_parent); + g_free (watcher->dest_basename); + + g_mutex_clear (&watcher->mutex); + g_mutex_clear (&watcher->ready_mutex); + g_mutex_clear (&watcher->done_mutex); + g_cond_clear (&watcher->ready_cond); + g_cond_clear (&watcher->done_cond); + + g_object_unref (watcher->dest); + g_object_unref (watcher->cancellable); + g_object_unref (watcher->progress); + + g_free (watcher); +} \ No newline at end of file diff --git a/libnemo-private/nemo-file-watcher.h b/libnemo-private/nemo-file-watcher.h new file mode 100644 index 000000000..48a6313de --- /dev/null +++ b/libnemo-private/nemo-file-watcher.h @@ -0,0 +1,91 @@ +// this is not working - just for illustration + +/*The `FileWatcher` approach fails because the problem it tries to solve — tracking how many bytes have actually been physically written to the device — has no reliable per-file kernel interface on Linux. + +The specific failure chain: + +1. **inotify** correctly detects the `.goutputstream-*` temp file and `WATCHER_STATE_MONITORING_TEMPFILE` correctly stats its size during the copy phase + +2. **`WATCHER_STATE_FLUSH_WAIT` exits immediately** because `g_file_query_info` returns the full file size as soon as GIO renames the temp file to the final destination — the inode size is updated at rename time, not when the data is physically flushed. So `actual_size >= expected_total_bytes` is true instantly and the watcher finishes without waiting for the real flush + +3. **No per-file flush progress exists** in the kernel. The alternatives all have disqualifying problems: + - `/proc/self/io` `write_bytes` is process-wide, polluted by other threads + - `/sys/block//stat` is device-wide, polluted by other processes + - `mincore()` reports pages in cache, not pages flushed to device + - `sync_file_range()` works on ext4/XFS but is unreliable or a no-op on FAT32/exFAT/NTFS — exactly the filesystems found on consumer USB devices + +4. **The only reliable completion signal is `fsync()`** but it blocks entirely with no intermediate progress + +The root cause is that the kernel page cache is intentionally opaque at the per-file level — it was never designed to expose per-file writeback progress to userspace. The synchronous copy approach sidesteps this entirely by never letting data enter the page cache unbounded — `fdatasync` per chunk means the byte counter is always ground truth.*/ + + +#ifndef NEMO_FILE_WATCHER_H +#define NEMO_FILE_WATCHER_H + +#include +#include +#include "nemo-progress-info.h" + +G_BEGIN_DECLS + +typedef enum { + WATCHER_STATE_WAITING_FOR_TEMPFILE, + WATCHER_STATE_MONITORING_TEMPFILE, + WATCHER_STATE_FLUSH_WAIT, + WATCHER_STATE_DONE +} WatcherState; + +typedef struct { + GMutex mutex; + + /* written by progress callback (copy job thread), read by watcher thread */ + goffset current_num_bytes; + goffset total_num_bytes; + + /* written by watcher thread on query failure, read by progress callback */ + gboolean error; + + /* set by file_watcher_set_copy_done after g_file_copy/move returns */ + gboolean copy_done; + + /* watcher thread */ + GThread *thread; + GFile *dest; + GFile *dest_parent; + char *dest_basename; + GFile *temp_file; + goffset expected_total_bytes; + GCancellable *cancellable; + NemoProgressInfo *progress; + gboolean *progress_done; + + /* inotify */ + int inotify_fd; + int watch_descriptor; + + /* copy thread blocks on this until inotify is armed */ + GMutex ready_mutex; + GCond ready_cond; + gboolean watcher_ready; + + /* copy job thread blocks on this after g_file_copy/move returns */ + GMutex done_mutex; + GCond done_cond; + gboolean watcher_done; +} FileWatcher; + +FileWatcher *file_watcher_new (GFile *dest, + goffset expected_total_bytes, + GCancellable *cancellable, + NemoProgressInfo *progress, + gboolean *progress_done); + +void file_watcher_start (FileWatcher *watcher); +void file_watcher_wait_ready (FileWatcher *watcher); +void file_watcher_set_copy_done (FileWatcher *watcher); +void file_watcher_wait (FileWatcher *watcher); +void file_watcher_free (FileWatcher *watcher); + +G_END_DECLS + +#endif /* NEMO_FILE_WATCHER_H */ \ No newline at end of file diff --git a/libnemo-private/nemo-gfile.c b/libnemo-private/nemo-gfile.c new file mode 100644 index 000000000..9e3dbc50f --- /dev/null +++ b/libnemo-private/nemo-gfile.c @@ -0,0 +1,628 @@ +/* nemo-gfile.c + * + * Copyright (C) 2006-2023 Red Hat, Inc. + * Copyright (C) 2026 Nemo Project + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + */ + +#include "nemo-gfile.h" +#include "glibconfig.h" +#include +#include +#include /* For GIOError, g_io_error_from_errno, etc. */ +#include /* For GError, g_set_error, etc. */ +#include +#include /* For g_file_error_from_errno (deprecated, but included for compatibility) */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Helper function to safely call pathconf */ +static long +safe_pathconf (const char *path, int name) +{ + long result = pathconf (path, name); + if (result == -1 && errno == 0) { + return -1; /* Parameter not supported */ + } + return result; +} + +/* Helper function to get the minimum recommended buffer size from the + * filesystem */ +static size_t +get_min_buffer_size (const char *path) +{ + if (path == NULL) { + return NEMO_G_FILE_MIN_BUFFER_SIZE; + } + + /* Try POSIX recommended increment for file transfers */ + long rec_increment = safe_pathconf (path, _PC_REC_INCR_XFER_SIZE); + if (rec_increment > 0) { + return MIN (MAX (NEMO_G_FILE_MIN_BUFFER_SIZE, + (size_t)rec_increment), + NEMO_G_FILE_MAX_BUFFER_SIZE); + } + + /* Try Linux-specific minimum allocation size */ + long alloc_size_min = safe_pathconf (path, _PC_ALLOC_SIZE_MIN); + if (alloc_size_min > 0) { + return MIN (MAX (NEMO_G_FILE_MIN_BUFFER_SIZE, + (size_t)alloc_size_min), + NEMO_G_FILE_MAX_BUFFER_SIZE); + } + + /* Fall back to statvfs */ + struct statvfs fs_info; + if (statvfs (path, &fs_info) == 0) { + size_t block_size = + fs_info.f_frsize; /* Filesystem fragment size */ + return MIN (MAX (NEMO_G_FILE_MIN_BUFFER_SIZE, block_size * 256), + NEMO_G_FILE_MAX_BUFFER_SIZE); + /* At least 1MB, or 256x block size, but not exceeding + * NEMO_G_FILE_MAX_BUFFER_SIZE */ + } + + /* Final fallback */ + return NEMO_G_FILE_MIN_BUFFER_SIZE; +} + +static void +copy_file_metadata (GFile *source, + GFile *destination, + GFileCopyFlags flags, + GCancellable *cancellable) +{ + /* No error parameter — metadata failure is never fatal */ + char *attrs_to_read = g_file_build_attribute_list_for_copy ( + destination, flags, cancellable, NULL); + if (!attrs_to_read) + return; + + GFileInfo *info = + g_file_query_info (source, + attrs_to_read, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable, + NULL); /* errors ignored */ + g_free (attrs_to_read); + + if (!info) + return; + + /* Ignore errors — failure to copy metadata is not a hard error */ + g_file_set_attributes_from_info (destination, + info, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable, + NULL); + + g_object_unref (info); +} + +static gboolean +files_on_same_filesystem (GFile *file1, GFile *file2, GError **error) +{ + struct stat stat1, stat2; + gchar *path1 = NULL; + gchar *path2 = NULL; + gboolean ret = FALSE; + + path1 = g_file_get_path (file1); + path2 = g_file_get_path (file2); + + if (!path1) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_FILENAME, + "Source file is not a local file"); + g_free (path2); + return FALSE; + } + if (!path2) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_FILENAME, + "Destination file is not a local file"); + g_free (path1); + return FALSE; + } + + if (stat (path1, &stat1) != 0) { + g_set_error (error, + G_IO_ERROR, + g_io_error_from_errno (errno), + "Failed to stat source file: %s", + g_strerror (errno)); + goto cleanup; + } + + /* Try destination directly first; if it doesn't exist yet, + * fall back to its parent directory, which must exist and + * will be on the same filesystem as the eventual destination. */ + if (stat (path2, &stat2) != 0) { + if (errno != ENOENT) { + g_set_error (error, + G_IO_ERROR, + g_io_error_from_errno (errno), + "Failed to stat destination file: %s", + g_strerror (errno)); + goto cleanup; + } + + /* Destination doesn't exist — stat its parent instead */ + GFile *dest_parent = g_file_get_parent (file2); + if (!dest_parent) { + g_set_error ( + error, + G_IO_ERROR, + G_IO_ERROR_INVALID_FILENAME, + "Destination file has no parent directory"); + goto cleanup; + } + + gchar *parent_path = g_file_get_path (dest_parent); + g_object_unref (dest_parent); + + if (!parent_path) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_FILENAME, + "Destination parent is not a local path"); + goto cleanup; + } + + if (stat (parent_path, &stat2) != 0) { + g_set_error ( + error, + G_IO_ERROR, + g_io_error_from_errno (errno), + "Failed to stat destination parent directory: %s", + g_strerror (errno)); + g_free (parent_path); + goto cleanup; + } + g_free (parent_path); + } + + ret = (stat1.st_dev == stat2.st_dev); + +cleanup: + g_free (path1); + g_free (path2); + return ret; +} + +gboolean +nemo_g_file_copy_to_blk_sync (GFile *source, + GFile *destination, + GFileCopyFlags flags, + GCancellable *cancellable, + NemoGFileProgressCallback progress_callback, + gpointer progress_callback_data, + GError **error) +{ + gchar *src_path, *dest_path; + int src_fd = -1, dest_fd = -1; + struct stat src_stat; + goffset total_size = 0; + goffset bytes_copied = 0; + guchar *buffer = NULL; + ssize_t bytes_read, bytes_written; + gboolean success = FALSE; + + const guint64 target_chunk_time_us = + NEMO_G_FILE_TARGET_MAX_CHUNK_TIME_US; + guint64 last_progress_time_us = 0; + guint64 last_chunk_time_us = 0; + guint64 chunk_time_us = 0; + size_t buffer_size; + int buffer_adjustment_steps = 0; + int new_buffer_trial_counter = 0; + const int max_buffer_adjustment_steps = 7; + gboolean buffer_size_found = FALSE; + gboolean backup_created = FALSE; + + /* Resolve paths */ + src_path = g_file_get_path (source); + if (!src_path) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_FILENAME, + "Source file is not a local file"); + return FALSE; + } + + dest_path = g_file_get_path (destination); + if (!dest_path) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_FILENAME, + "Destination file is not a local file"); + g_free (src_path); + return FALSE; + } + + /* Get minimum buffer size from filesystem */ + buffer_size = get_min_buffer_size (dest_path); + + /* Open source file */ + int src_flags = O_RDONLY; + if (flags & G_FILE_COPY_NOFOLLOW_SYMLINKS) { + src_flags |= O_NOFOLLOW; + } + src_fd = open (src_path, src_flags); + if (src_fd == -1) { + if (errno == ELOOP) { + /* Source is a symlink and we're not following it — + * block copy of symlinks is not supported */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "Cannot block-copy a symlink source: %s", + src_path); + } else { + g_set_error (error, + G_IO_ERROR, + g_io_error_from_errno (errno), + "Failed to open source file: %s", + g_strerror (errno)); + } + g_free (src_path); + g_free (dest_path); + return FALSE; + } + + /* Get source file size */ + if (fstat (src_fd, &src_stat) == -1) { + g_set_error (error, + G_IO_ERROR, + g_io_error_from_errno (errno), + "Failed to stat source file: %s", + g_strerror (errno)); + goto cleanup; + } + total_size = src_stat.st_size; + + /* Handle backup before opening destination */ + if ((flags & G_FILE_COPY_OVERWRITE) && (flags & G_FILE_COPY_BACKUP)) { + struct stat dest_stat; + if (stat (dest_path, &dest_stat) == 0) { + gchar *backup_path = g_strconcat (dest_path, "~", NULL); + + if (unlink (backup_path) != 0 && errno != ENOENT) { + g_set_error ( + error, + G_IO_ERROR, + g_io_error_from_errno (errno), + "Failed to remove existing backup file: %s", + g_strerror (errno)); + g_free (backup_path); + goto cleanup; + } + + if (rename (dest_path, backup_path) != 0) { + g_set_error (error, + G_IO_ERROR, + g_io_error_from_errno (errno), + "Failed to create backup file: %s", + g_strerror (errno)); + g_free (backup_path); + goto cleanup; + } + + g_free (backup_path); + + /* Destination has been moved to backup — create fresh + * instead of truncating */ + backup_created = TRUE; + } + /* If destination doesn't exist, no backup needed */ + } + + /* Open destination file */ + int dest_flags = O_WRONLY | O_CREAT; + if ((flags & G_FILE_COPY_OVERWRITE) && !backup_created) { + dest_flags |= O_TRUNC; + } else { + dest_flags |= O_EXCL; + } + dest_flags |= + O_SYNC; /* Synchronous writes (report only actually written bytes) */ + + /* Set destination file permissions */ + mode_t dest_mode = (flags & G_FILE_COPY_TARGET_DEFAULT_PERMS) ? + 0666 : + (src_stat.st_mode & 0777); + dest_fd = open (dest_path, dest_flags, dest_mode); + if (dest_fd == -1) { + g_set_error (error, + G_IO_ERROR, + g_io_error_from_errno (errno), + "Failed to open destination file: %s", + g_strerror (errno)); + goto cleanup; + } + + /* Allocate buffer */ + buffer = g_malloc (buffer_size); + if (!buffer) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Failed to allocate buffer"); + goto cleanup; + } + + g_debug ("Initial buffer size for blk copy: %zu bytes (%zu MB)", + buffer_size, + buffer_size / 1024 / 1024); + + last_progress_time_us = g_get_monotonic_time (); + last_chunk_time_us = last_progress_time_us; + + /* Copy data in chunks */ + while ((bytes_read = read (src_fd, buffer, buffer_size)) > 0) { + /* Check for cancellation */ + if (cancellable && + g_cancellable_set_error_if_cancelled (cancellable, error)) { + goto cleanup; + } + + /* Handle partial writes */ + size_t bytes_to_write = bytes_read; + size_t total_written = 0; + + while (total_written < bytes_to_write) { + bytes_written = write (dest_fd, + buffer + total_written, + bytes_to_write - total_written); + if (bytes_written == -1) { + if (errno == EINTR) { + continue; /* Retry on interrupt */ + } + g_set_error ( + error, + G_IO_ERROR, + g_io_error_from_errno (errno), + "Failed to write to destination: %s", + g_strerror (errno)); + goto cleanup; + } + total_written += bytes_written; + } + + bytes_copied += total_written; + + /* Measure chunk time and adapt buffer size */ + guint64 current_time_us = g_get_monotonic_time (); + chunk_time_us = current_time_us - last_chunk_time_us; + last_chunk_time_us = current_time_us; + + /* Adaptive buffer sizing (grow only - power of 2 - try new size n + * times) */ + if (!buffer_size_found && (goffset)buffer_size < total_size && + buffer_adjustment_steps < max_buffer_adjustment_steps && + buffer_size * 2 <= NEMO_G_FILE_MAX_BUFFER_SIZE && + new_buffer_trial_counter < NEMO_G_FILE_BUFFER_TRIALS && + bytes_copied < total_size) { + if (chunk_time_us < target_chunk_time_us) { + gpointer new_buffer = + g_try_malloc (buffer_size * 2); + if (!new_buffer) { + buffer_size_found = TRUE; + g_debug ( + "Final buffer size used for blk copy: %zu bytes " + "(%zu MB)", + buffer_size, + buffer_size / 1024 / 1024); + } else { + g_free (buffer); + buffer = new_buffer; + buffer_size *= 2; + buffer_adjustment_steps++; + } + new_buffer_trial_counter = 0; + } else { + new_buffer_trial_counter++; + } + + } else if (!buffer_size_found) { + buffer_size_found = TRUE; + new_buffer_trial_counter = 0; + g_debug ( + "Final buffer size used for blk copy: %zu bytes (%zu MB)", + buffer_size, + buffer_size / 1024 / 1024); + } + + if (progress_callback) { + guint64 elapsed_us = + current_time_us - last_progress_time_us; + if (elapsed_us >= target_chunk_time_us || + bytes_copied == total_size) { + progress_callback (bytes_copied, + total_size, + progress_callback_data); + last_progress_time_us = current_time_us; + } + } + } + + if (bytes_read == -1) { + g_set_error (error, + G_IO_ERROR, + g_io_error_from_errno (errno), + "Failed to read from source: %s", + g_strerror (errno)); + goto cleanup; + } + + /* Copy metadata if requested */ + copy_file_metadata (source, destination, flags, cancellable); + + success = TRUE; + +cleanup: + if (src_fd != -1) + close (src_fd); + if (dest_fd != -1) { + if (!success && + (!(flags & G_FILE_COPY_OVERWRITE) || backup_created)) + unlink (dest_path); + close (dest_fd); + } + + /* Restore backup if copy failed */ + if (!success && backup_created) { + gchar *backup_path = g_strconcat (dest_path, "~", NULL); + if (rename (backup_path, dest_path) != 0) { + g_warning ("Failed to restore backup '%s' to '%s': %s", + backup_path, + dest_path, + g_strerror (errno)); + } + g_free (backup_path); + } + + g_free (buffer); + g_free (src_path); + g_free (dest_path); + + /* Ensure 100% progress is always reported on success */ + if (success && progress_callback) + progress_callback ( + total_size, total_size, progress_callback_data); + + return success; +} + +gboolean +nemo_g_file_move_to_blk_sync (GFile *source, + GFile *destination, + GFileCopyFlags flags, + GCancellable *cancellable, + NemoGFileProgressCallback progress_callback, + gpointer progress_callback_data, + GError **error) +{ + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + + if (flags & G_FILE_COPY_NO_FALLBACK_FOR_MOVE) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "Operation not supported"); + return FALSE; + } + + /* Attempt atomic rename if on the same filesystem */ + GError *same_fs_error = NULL; + if (files_on_same_filesystem (source, destination, &same_fs_error)) { + gchar *src_path = g_file_get_path (source); + gchar *dest_path = g_file_get_path (destination); + + if (!src_path || !dest_path) { + g_free (src_path); + g_free (dest_path); + g_set_error ( + error, + G_IO_ERROR, + G_IO_ERROR_INVALID_FILENAME, + "Source or destination is not a local file"); + return FALSE; + } + + gboolean renamed = FALSE; + + if (flags & G_FILE_COPY_OVERWRITE) { + /* Plain rename — atomically replaces destination if it exists */ + if (rename (src_path, dest_path) == 0) { + renamed = TRUE; + } else { + g_debug ( + "rename() failed (%s), falling back to copy+delete", + g_strerror (errno)); + } + } else { + /* Use renameat2 with RENAME_NOREPLACE for atomic no-clobber */ + if (syscall (SYS_renameat2, + AT_FDCWD, + src_path, + AT_FDCWD, + dest_path, + RENAME_NOREPLACE) == 0) { + renamed = TRUE; + } else if (errno == EEXIST) { + g_free (src_path); + g_free (dest_path); + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_EXISTS, + "Destination file already exists"); + return FALSE; + } else if (errno == ENOSYS) { + g_debug ( + "renameat2 not available, falling back to copy+delete"); + } else { + g_debug ( + "renameat2() failed (%s), falling back to copy+delete", + g_strerror (errno)); + } + } + + g_free (src_path); + g_free (dest_path); + + if (renamed) { + if (progress_callback) + progress_callback ( + 1, 1, progress_callback_data); + return TRUE; + } + + } else if (same_fs_error) { + g_debug ( + "files_on_same_filesystem failed: %s, falling back to copy+delete", + same_fs_error->message); + g_clear_error (&same_fs_error); + } + + /* Fall back to copy+delete for cross-filesystem moves or + * when rename is unavailable */ + flags |= G_FILE_COPY_ALL_METADATA | G_FILE_COPY_NOFOLLOW_SYMLINKS; + if (!nemo_g_file_copy_to_blk_sync (source, + destination, + flags, + cancellable, + progress_callback, + progress_callback_data, + error)) + return FALSE; + + return g_file_delete (source, cancellable, error); +} \ No newline at end of file diff --git a/libnemo-private/nemo-gfile.h b/libnemo-private/nemo-gfile.h new file mode 100644 index 000000000..94893e045 --- /dev/null +++ b/libnemo-private/nemo-gfile.h @@ -0,0 +1,57 @@ +/* nemo-gfile.h + * + * Copyright (C) 2026 Nemo Project + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + */ + +#ifndef __NEMO_GFILE_H__ +#define __NEMO_GFILE_H__ + +#include + +G_BEGIN_DECLS + +#define NEMO_G_FILE_TARGET_MAX_CHUNK_TIME_US 500000 +#define NEMO_G_FILE_MIN_BUFFER_SIZE 1024 * 1024 +#define NEMO_G_FILE_MAX_BUFFER_SIZE 1024 * 1024 * 256 +#define NEMO_G_FILE_BUFFER_TRIALS 3 + +typedef void (*NemoGFileProgressCallback) (goffset current_num_bytes, + goffset total_num_bytes, + gpointer user_data); + +gboolean +nemo_g_file_copy_to_blk_sync (GFile *source, + GFile *destination, + GFileCopyFlags flags, + GCancellable *cancellable, + NemoGFileProgressCallback progress_callback, + gpointer progress_callback_data, + GError **error); + +gboolean +nemo_g_file_move_to_blk_sync (GFile *source, + GFile *destination, + GFileCopyFlags flags, + GCancellable *cancellable, + NemoGFileProgressCallback progress_callback, + gpointer progress_callback_data, + GError **error); + +G_END_DECLS + +#endif /* __NEMO_GFILE_H__ */ \ No newline at end of file diff --git a/meson.build b/meson.build index d6cf1d285..1475c8429 100644 --- a/meson.build +++ b/meson.build @@ -44,7 +44,7 @@ conf.set10('HAVE_MALLOPT', cc.has_function('mallopt', prefix: '#include =3.10.0') -gio = dependency('gio-2.0', version: glib_version) -gio_unix= dependency('gio-unix-2.0', version: glib_version) -glib = dependency('glib-2.0', version: glib_version) -gmodule = dependency('gmodule-no-export-2.0', version: glib_version) -gobject = dependency('gobject-2.0', version: '>=2.0') -go_intr = dependency('gobject-introspection-1.0', version: '>=1.0') -json = dependency('json-glib-1.0', version: '>=1.6') +# Get the absolute path to the local glib install +glib_install_path = run_command( + 'realpath', + meson.current_source_dir() / '..' / 'glib' / 'install', + check: true +).stdout().strip() + + +glib_custom = declare_dependency( + dependencies: [ + dependency('glib-2.0', version: glib_version), + dependency('gobject-2.0', version: '>=2.0'), + dependency('gio-2.0', version: glib_version), + dependency('gio-unix-2.0', version: glib_version), + dependency('gmodule-no-export-2.0', version: glib_version), + ], +) +# Use the overridden dependencies for glib components +glib = glib_custom +gobject = glib_custom +gio = glib_custom +gio_unix = glib_custom +gmodule = glib_custom + +# Keep other dependencies as before +gtk = dependency('gtk+-3.0', version: '>=3.10.0') cinnamon= dependency('cinnamon-desktop', version: '>=4.8.0') gail = dependency('gail-3.0') x11 = dependency('x11') xapp = dependency('xapp', version: '>=2.0.0') +json = dependency('json-glib-1.0', version: '>=1.6') +go_intr = dependency('gobject-introspection-1.0', version: '>=1.0') # Facultative dependencies