From 3e940ae79a7c1d578a7086fa5aa1c2a0fb47e377 Mon Sep 17 00:00:00 2001 From: "Yury V. Zaytsev" Date: Mon, 9 Jun 2025 07:33:30 +0200 Subject: [PATCH 1/4] Ticket #4720: delete resource forks on macOS last to avoid recursive delete failures Signed-off-by: Yury V. Zaytsev --- lib/fs.h | 5 +++++ src/filemanager/file.c | 26 ++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/fs.h b/lib/fs.h index 8c5d26f7fc..d8490aa6a3 100644 --- a/lib/fs.h +++ b/lib/fs.h @@ -108,6 +108,11 @@ #define DIR_IS_DOT(x) ((x)[0] == '.' && (x)[1] == '\0') #define DIR_IS_DOTDOT(x) ((x)[0] == '.' && (x)[1] == '.' && (x)[2] == '\0') +/* On macOS, if a file system doesn't support resource forks, metadata is automatically stored in + * shadow files beginning with `._`. They are automatically managed by the OS and will reappear if + * deleted. */ +#define FILE_IS_RESOURCE_FORK(x) ((x)[0] == '.' && (x)[1] == '_' && (x)[2] != '\0') + /*** enums ***************************************************************************************/ /*** structures declarations (and typedefs of structures)*****************************************/ diff --git a/src/filemanager/file.c b/src/filemanager/file.c index 398bfa85f1..afa570e112 100644 --- a/src/filemanager/file.c +++ b/src/filemanager/file.c @@ -1436,9 +1436,14 @@ try_erase_dir (file_op_context_t *ctx, const vfs_path_t *vpath) abort -> cancel stack ignore -> warn every level, gets default ignore_all -> remove as much as possible + + This function should either be called with delete_resource_forks = TRUE always, or called twice, + first with FALSE and then with TRUE to make sure that no errors pop up due to resource forks being + deleted on macOS and then being re-created on the fly by the OS. */ static FileProgressStatus -recursive_erase (file_op_context_t *ctx, const vfs_path_t *vpath) +recursive_erase (file_op_context_t *ctx, const vfs_path_t *vpath, + const gboolean delete_resource_forks) { struct vfs_dirent *next; DIR *reading; @@ -1464,9 +1469,18 @@ recursive_erase (file_op_context_t *ctx, const vfs_path_t *vpath) return FILE_RETRY; } if (S_ISDIR (buf.st_mode)) - return_status = recursive_erase (ctx, tmp_vpath); + { + return_status = recursive_erase (ctx, tmp_vpath, FALSE); + if (return_status != FILE_ABORT) + return_status = recursive_erase (ctx, tmp_vpath, TRUE); + } else - return_status = erase_file (ctx, tmp_vpath); + { + if (delete_resource_forks || !FILE_IS_RESOURCE_FORK (next->d_name)) + return_status = erase_file (ctx, tmp_vpath); + else + return_status = FILE_SKIP; + } vfs_path_free (tmp_vpath, TRUE); } mc_closedir (reading); @@ -3390,7 +3404,11 @@ erase_dir (file_op_context_t *ctx, const vfs_path_t *vpath) // not empty error = query_recursive (ctx, vfs_path_as_str (vpath)); if (error == FILE_CONT) - error = recursive_erase (ctx, vpath); + { + error = recursive_erase (ctx, vpath, FALSE); + if (error != FILE_ABORT) + error = recursive_erase (ctx, vpath, TRUE); + } return error; } From 6040ec4990ea2e697f497eca37519aacab1758f5 Mon Sep 17 00:00:00 2001 From: "Yury V. Zaytsev" Date: Sun, 10 May 2026 15:08:10 +0200 Subject: [PATCH 2/4] fixup! Ticket #4720: delete resource forks on macOS last to avoid recursive delete failures Signed-off-by: Yury V. Zaytsev --- src/filemanager/file.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filemanager/file.c b/src/filemanager/file.c index afa570e112..8aa95c67df 100644 --- a/src/filemanager/file.c +++ b/src/filemanager/file.c @@ -1479,7 +1479,7 @@ recursive_erase (file_op_context_t *ctx, const vfs_path_t *vpath, if (delete_resource_forks || !FILE_IS_RESOURCE_FORK (next->d_name)) return_status = erase_file (ctx, tmp_vpath); else - return_status = FILE_SKIP; + return_status = FILE_CONT; } vfs_path_free (tmp_vpath, TRUE); } From bef4340dabe11585e10aca546780f08a9b11432e Mon Sep 17 00:00:00 2001 From: Andrew Borodin Date: Fri, 15 May 2026 19:19:19 +0300 Subject: [PATCH 3/4] fixup! Ticket #4720: delete resource forks on macOS last to avoid recursive delete failures --- src/filemanager/file.c | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/filemanager/file.c b/src/filemanager/file.c index 8aa95c67df..0dfe759b1b 100644 --- a/src/filemanager/file.c +++ b/src/filemanager/file.c @@ -174,6 +174,8 @@ static const char *prompt_parts[] = { /*** forward declarations (file scope functions) *************************************************/ +static FileProgressStatus erase_dir_iff_empty (file_op_context_t *ctx, const vfs_path_t *vpath); + /*** file scope variables ************************************************************************/ /* the hard link cache */ @@ -1469,20 +1471,15 @@ recursive_erase (file_op_context_t *ctx, const vfs_path_t *vpath, return FILE_RETRY; } if (S_ISDIR (buf.st_mode)) - { - return_status = recursive_erase (ctx, tmp_vpath, FALSE); - if (return_status != FILE_ABORT) - return_status = recursive_erase (ctx, tmp_vpath, TRUE); - } + return_status = recursive_erase (ctx, tmp_vpath, delete_resource_forks); + else if (delete_resource_forks || !FILE_IS_RESOURCE_FORK (next->d_name)) + return_status = erase_file (ctx, tmp_vpath); else - { - if (delete_resource_forks || !FILE_IS_RESOURCE_FORK (next->d_name)) - return_status = erase_file (ctx, tmp_vpath); - else - return_status = FILE_CONT; - } + return_status = FILE_SKIP; + vfs_path_free (tmp_vpath, TRUE); } + mc_closedir (reading); if (return_status == FILE_ABORT) @@ -1495,7 +1492,7 @@ recursive_erase (file_op_context_t *ctx, const vfs_path_t *vpath, mc_refresh (); - return try_erase_dir (ctx, vpath); + return erase_dir_iff_empty (ctx, vpath); } /* --------------------------------------------------------------------------------------------- */ From 48d633d31cf9ab2168bb11a0613b93b8f2211d00 Mon Sep 17 00:00:00 2001 From: Andrew Borodin Date: Fri, 15 May 2026 18:27:14 +0300 Subject: [PATCH 4/4] src/filemanager/file.c: reorder routines to get rid of forward declaration. No functional changes. Signed-off-by: Andrew Borodin --- src/filemanager/file.c | 165 ++++++++++++++++++++--------------------- 1 file changed, 82 insertions(+), 83 deletions(-) diff --git a/src/filemanager/file.c b/src/filemanager/file.c index 0dfe759b1b..5a09f0bfa1 100644 --- a/src/filemanager/file.c +++ b/src/filemanager/file.c @@ -174,8 +174,6 @@ static const char *prompt_parts[] = { /*** forward declarations (file scope functions) *************************************************/ -static FileProgressStatus erase_dir_iff_empty (file_op_context_t *ctx, const vfs_path_t *vpath); - /*** file scope variables ************************************************************************/ /* the hard link cache */ @@ -1413,6 +1411,62 @@ erase_file (file_op_context_t *ctx, const vfs_path_t *vpath) /* --------------------------------------------------------------------------------------------- */ +/** + * Check if directory is empty or not. + * + * @param ctx file operation context descriptor + * @param vpath directory handler + * @param error status of directory reading + * + * @return -1 on error, + * 1 if there are no entries besides "." and ".." in the directory path points to, + * 0 else. + * + * ATTENTION! Be careful when modifying this function (like commit 25e419ba0886f)! + * Some implementations of readdir() in MC VFS (for example, vfs_s_readdir(), which is used + * in SHELL) don't return "." and ".." entries. + */ +static int +check_dir_is_empty (file_op_context_t *ctx, const vfs_path_t *vpath, FileProgressStatus *error) +{ + DIR *dir; + struct vfs_dirent *d; + int i = 1; + + while ((dir = mc_opendir (vpath)) == NULL) + { + if (ctx->ignore_all) + *error = FILE_IGNORE_ALL; + else + { + const FileProgressStatus status = file_error ( + ctx, TRUE, _ ("Cannot enter into directory\n%s"), vfs_path_as_str (vpath)); + + if (status == FILE_RETRY) + continue; + if (status == FILE_IGNORE_ALL) + ctx->ignore_all = TRUE; + + *error = status; // FILE_IGNORE, FILE_IGNORE_ALL, FILE_ABORT + } + + return (-1); + } + + for (d = mc_readdir (dir); d != NULL; d = mc_readdir (dir)) + if (!DIR_IS_DOT (d->d_name) && !DIR_IS_DOTDOT (d->d_name)) + { + i = 0; + break; + } + + mc_closedir (dir); + *error = FILE_CONT; + return i; +} + +/* --------------------------------------------------------------------------------------------- */ + static FileProgressStatus try_erase_dir (file_op_context_t *ctx, const vfs_path_t *vpath) { @@ -1433,6 +1487,32 @@ try_erase_dir (file_op_context_t *ctx, const vfs_path_t *vpath) /* --------------------------------------------------------------------------------------------- */ +static FileProgressStatus +erase_dir_iff_empty (file_op_context_t *ctx, const vfs_path_t *vpath) +{ + FileProgressStatus error = FILE_CONT; + + file_progress_show_deleting (ctx, vpath, NULL); + file_progress_show_count (ctx); + if (file_progress_check_buttons (ctx) == FILE_ABORT) + return FILE_ABORT; + + mc_refresh (); + + const int res = check_dir_is_empty (ctx, vpath, &error); + + if (res == -1) + return error; + + if (res != 1) + return FILE_CONT; + + // not empty or error + return try_erase_dir (ctx, vpath); +} + +/* --------------------------------------------------------------------------------------------- */ + /** Recursive removal of files abort -> cancel stack @@ -1495,87 +1575,6 @@ recursive_erase (file_op_context_t *ctx, const vfs_path_t *vpath, return erase_dir_iff_empty (ctx, vpath); } -/* --------------------------------------------------------------------------------------------- */ -/** - * Check if directory is empty or not. - * - * @param ctx file operation context descriptor - * @param vpath directory handler - * @param error status of directory reading - * - * @return -1 on error, - * 1 if there are no entries besides "." and ".." in the directory path points to, - * 0 else. - * - * ATTENTION! Be careful when modifying this function (like commit 25e419ba0886f)! - * Some implementations of readdir() in MC VFS (for example, vfs_s_readdir(), which is used - * in SHELL) don't return "." and ".." entries. - */ -static int -check_dir_is_empty (file_op_context_t *ctx, const vfs_path_t *vpath, FileProgressStatus *error) -{ - DIR *dir; - struct vfs_dirent *d; - int i = 1; - - while ((dir = mc_opendir (vpath)) == NULL) - { - if (ctx->ignore_all) - *error = FILE_IGNORE_ALL; - else - { - const FileProgressStatus status = file_error ( - ctx, TRUE, _ ("Cannot enter into directory\n%s"), vfs_path_as_str (vpath)); - - if (status == FILE_RETRY) - continue; - if (status == FILE_IGNORE_ALL) - ctx->ignore_all = TRUE; - - *error = status; // FILE_IGNORE, FILE_IGNORE_ALL, FILE_ABORT - } - - return (-1); - } - - for (d = mc_readdir (dir); d != NULL; d = mc_readdir (dir)) - if (!DIR_IS_DOT (d->d_name) && !DIR_IS_DOTDOT (d->d_name)) - { - i = 0; - break; - } - - mc_closedir (dir); - *error = FILE_CONT; - return i; -} - -/* --------------------------------------------------------------------------------------------- */ - -static FileProgressStatus -erase_dir_iff_empty (file_op_context_t *ctx, const vfs_path_t *vpath) -{ - FileProgressStatus error = FILE_CONT; - - file_progress_show_deleting (ctx, vpath, NULL); - file_progress_show_count (ctx); - if (file_progress_check_buttons (ctx) == FILE_ABORT) - return FILE_ABORT; - - mc_refresh (); - - const int res = check_dir_is_empty (ctx, vpath, &error); - - if (res == -1) - return error; - - if (res != 1) - return FILE_CONT; - - // not empty or error - return try_erase_dir (ctx, vpath); -} - /* --------------------------------------------------------------------------------------------- */ static void