/*
   File management.

   Copyright (C) 1994-2025
   Free Software Foundation, Inc.

   Written by:
   Janne Kukonlehto, 1994, 1995
   Fred Leeflang, 1994, 1995
   Miguel de Icaza, 1994, 1995, 1996
   Jakub Jelinek, 1995, 1996
   Norbert Warmuth, 1997
   Pavel Machek, 1998
   Andrew Borodin <aborodin@vmail.ru>, 2011-2022

   The copy code was based in GNU's cp, and was written by:
   Torbjorn Granlund, David MacKenzie, and Jim Meyering.

   The move code was based in GNU's mv, and was written by:
   Mike Parker and David MacKenzie.

   Janne Kukonlehto added much error recovery to them for being used
   in an interactive program.

   This file is part of the Midnight Commander.

   The Midnight Commander is free software: you can redistribute it
   and/or modify it under the terms of the GNU General Public License as
   published by the Free Software Foundation, either version 3 of the License,
   or (at your option) any later version.

   The Midnight Commander 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 General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * Please note that all dialogs used here must be safe for background
 * operations.
 */

/** \file src/filemanager/file.c
 *  \brief Source: file management
 */

/* {{{ Include files */

#include <config.h>

#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include "lib/global.h"
#include "lib/tty/tty.h"
#include "lib/tty/key.h"
#include "lib/search.h"
#include "lib/strutil.h"
#include "lib/util.h"
#include "lib/vfs/vfs.h"
#include "lib/vfs/utilvfs.h"
#include "lib/widget.h"

#include "src/setup.h"
#ifdef ENABLE_BACKGROUND
#include "src/background.h"     /* do_background() */
#endif

/* Needed for other_panel and WTree */
#include "dir.h"
#include "filenot.h"
#include "tree.h"
#include "filemanager.h"        /* other_panel */
#include "layout.h"             /* rotate_dash() */
#include "ioblksize.h"          /* io_blksize() */

#include "file.h"

/* }}} */

/*** global variables ****************************************************************************/

/* TRANSLATORS: no need to translate 'DialogTitle', it's just a context prefix  */
const char *op_names[3] = {
    N_("DialogTitle|Copy"),
    N_("DialogTitle|Move"),
    N_("DialogTitle|Delete")
};

/*** file scope macro definitions ****************************************************************/

#define FILEOP_UPDATE_INTERVAL 2
#define FILEOP_STALLING_INTERVAL 4
#define FILEOP_UPDATE_INTERVAL_US (FILEOP_UPDATE_INTERVAL * G_USEC_PER_SEC)
#define FILEOP_STALLING_INTERVAL_US (FILEOP_STALLING_INTERVAL * G_USEC_PER_SEC)

/*** file scope type declarations ****************************************************************/

/* This is a hard link cache */
typedef struct
{
    const struct vfs_class *vfs;
    dev_t dev;
    ino_t ino;
    mode_t st_mode;
    vfs_path_t *src_vpath;
    vfs_path_t *dst_vpath;
} link_t;

/* Status of the destination file */
typedef enum
{
    DEST_NONE = 0,              /**< Not created */
    DEST_SHORT_QUERY,           /**< Created, not fully copied, query to do */
    DEST_SHORT_KEEP,            /**< Created, not fully copied, keep it */
    DEST_SHORT_DELETE,          /**< Created, not fully copied, delete it */
    DEST_FULL                   /**< Created, fully copied */
} dest_status_t;

/* Status of hard link creation */
typedef enum
{
    HARDLINK_OK = 0,            /**< Hardlink was created successfully */
    HARDLINK_CACHED,            /**< Hardlink was added to the cache */
    HARDLINK_NOTLINK,           /**< This is not a hard link */
    HARDLINK_UNSUPPORTED,       /**< VFS doesn't support hard links */
    HARDLINK_ERROR,             /**< Hard link creation error */
    HARDLINK_ABORT              /**< Stop file operation after hardlink creation error */
} hardlink_status_t;

/*
 * This array introduced to avoid translation problems. The former (op_names)
 * is assumed to be nouns, suitable in dialog box titles; this one should
 * contain whatever is used in prompt itself (i.e. in russian, it's verb).
 * (I don't use spaces around the words, because someday they could be
 * dropped, when widgets get smarter)
 */

/* TRANSLATORS: no need to translate 'FileOperation', it's just a context prefix  */
static const char *op_names1[] = {
    N_("FileOperation|Copy"),
    N_("FileOperation|Move"),
    N_("FileOperation|Delete")
};

/*
 * These are formats for building a prompt. Parts encoded as follows:
 * %o - operation from op_names1
 * %f - file/files or files/directories, as appropriate
 * %m - "with source mask" or question mark for delete
 * %s - source name (truncated)
 * %d - number of marked files
 * %n - the '\n' symbol to form two-line prompt for delete or space for other operations
 */
/* xgettext:no-c-format */
static const char *one_format = N_("%o %f%n\"%s\"%m");
/* xgettext:no-c-format */
static const char *many_format = N_("%o %d %f%m");

static const char *prompt_parts[] = {
    N_("file"),
    N_("files"),
    N_("directory"),
    N_("directories"),
    N_("files/directories"),
    /* TRANSLATORS: keep leading space here to split words in Copy/Move dialog */
    N_(" with source mask:")
};

/*** forward declarations (file scope functions) *************************************************/

/*** file scope variables ************************************************************************/

/* the hard link cache */
static GSList *linklist = NULL;

/* the files-to-be-erased list */
static GQueue *erase_list = NULL;

/*
 * This list holds information about just created target directories and is used to detect
 * when an directory is copied into itself (we don't want to copy infinitely).
 */
static GSList *dest_dirs = NULL;

/* --------------------------------------------------------------------------------------------- */
/*** file scope functions ************************************************************************/
/* --------------------------------------------------------------------------------------------- */

static void
dirsize_status_locate_buttons (dirsize_status_msg_t *dsm)
{
    status_msg_t *sm = STATUS_MSG (dsm);
    Widget *wd = WIDGET (sm->dlg);
    int y, x;
    WRect r;

    y = wd->rect.y + 5;
    x = wd->rect.x;

    if (!dsm->allow_skip)
    {
        /* single button: "Abort" */
        x += (wd->rect.cols - dsm->abort_button->rect.cols) / 2;
        r = dsm->abort_button->rect;
        r.y = y;
        r.x = x;
        widget_set_size_rect (dsm->abort_button, &r);
    }
    else
    {
        /* two buttons: "Abort" and "Skip" */
        int cols;

        cols = dsm->abort_button->rect.cols + dsm->skip_button->rect.cols + 1;
        x += (wd->rect.cols - cols) / 2;
        r = dsm->abort_button->rect;
        r.y = y;
        r.x = x;
        widget_set_size_rect (dsm->abort_button, &r);
        x += dsm->abort_button->rect.cols + 1;
        r = dsm->skip_button->rect;
        r.y = y;
        r.x = x;
        widget_set_size_rect (dsm->skip_button, &r);
    }
}

/* --------------------------------------------------------------------------------------------- */

static char *
build_dest (file_op_context_t *ctx, const char *src, const char *dest, FileProgressStatus *status)
{
    char *s, *q;
    const char *fnsource;

    *status = FILE_CONT;

    s = g_strdup (src);

    /* We remove \n from the filename since regex routines would use \n as an anchor */
    /* this is just to be allowed to maniupulate file names with \n on it */
    for (q = s; *q != '\0'; q++)
        if (*q == '\n')
            *q = ' ';

    fnsource = x_basename (s);

    if (!mc_search_run (ctx->search_handle, fnsource, 0, strlen (fnsource), NULL))
    {
        q = NULL;
        *status = FILE_SKIP;
    }
    else
    {
        q = mc_search_prepare_replace_str2 (ctx->search_handle, ctx->dest_mask);
        if (ctx->search_handle->error != MC_SEARCH_E_OK)
        {
            if (ctx->search_handle->error_str != NULL)
                message (D_ERROR, MSG_ERROR, "%s", ctx->search_handle->error_str);

            *status = FILE_ABORT;
        }
    }

    MC_PTR_FREE (s);

    if (*status == FILE_CONT)
    {
        char *repl_dest;

        repl_dest = mc_search_prepare_replace_str2 (ctx->search_handle, dest);
        if (ctx->search_handle->error == MC_SEARCH_E_OK)
            s = mc_build_filename (repl_dest, q, (char *) NULL);
        else
        {
            if (ctx->search_handle->error_str != NULL)
                message (D_ERROR, MSG_ERROR, "%s", ctx->search_handle->error_str);

            *status = FILE_ABORT;
        }

        g_free (repl_dest);
    }

    g_free (q);

    return s;
}

/* --------------------------------------------------------------------------------------------- */

static void
free_link (void *data)
{
    link_t *lp = (link_t *) data;

    vfs_path_free (lp->src_vpath, TRUE);
    vfs_path_free (lp->dst_vpath, TRUE);
    g_free (lp);
}

/* --------------------------------------------------------------------------------------------- */

static inline void *
free_erase_list (GQueue *lp)
{
    if (lp != NULL)
        g_queue_free_full (lp, free_link);

    return NULL;
}

/* --------------------------------------------------------------------------------------------- */

static inline void *
free_linklist (GSList *lp)
{
    g_slist_free_full (lp, free_link);

    return NULL;
}

/* --------------------------------------------------------------------------------------------- */

static const link_t *
is_in_linklist (const GSList *lp, const vfs_path_t *vpath, const mc_stat_t *sb)
{
    const struct vfs_class *class;
    ino_t ino = sb->st_ino;
    dev_t dev = sb->st_dev;

    class = vfs_path_get_last_path_vfs (vpath);

    for (; lp != NULL; lp = (const GSList *) g_slist_next (lp))
    {
        const link_t *lnk = (const link_t *) lp->data;

        if (lnk->vfs == class && lnk->ino == ino && lnk->dev == dev)
            return lnk;
    }

    return NULL;
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Check and made hardlink
 *
 * @return FALSE if the inode wasn't found in the cache and TRUE if it was found
 * and a hardlink was successfully made
 */

static hardlink_status_t
check_hardlinks (file_op_context_t *ctx, const vfs_path_t *src_vpath, const mc_stat_t *src_stat,
                 const vfs_path_t *dst_vpath, gboolean *ignore_all)
{
    link_t *lnk;
    ino_t ino = src_stat->st_ino;
    dev_t dev = src_stat->st_dev;

    if (src_stat->st_nlink < 2)
        return HARDLINK_NOTLINK;
    if ((vfs_file_class_flags (src_vpath) & VFSF_NOLINKS) != 0)
        return HARDLINK_UNSUPPORTED;

    lnk = (link_t *) is_in_linklist (linklist, src_vpath, src_stat);
    if (lnk != NULL)
    {
        int stat_result;
        mc_stat_t link_stat;

        stat_result = mc_stat (lnk->src_vpath, &link_stat);

        if (stat_result == 0 && link_stat.st_ino == ino && link_stat.st_dev == dev)
        {
            const struct vfs_class *lp_name_class;
            const struct vfs_class *my_vfs;

            lp_name_class = vfs_path_get_last_path_vfs (lnk->src_vpath);
            my_vfs = vfs_path_get_last_path_vfs (src_vpath);

            if (lp_name_class == my_vfs)
            {
                const struct vfs_class *p_class, *dst_name_class;

                dst_name_class = vfs_path_get_last_path_vfs (dst_vpath);
                p_class = vfs_path_get_last_path_vfs (lnk->dst_vpath);

                if (dst_name_class == p_class)
                {
                    gboolean ok;

                    while (!(ok = (mc_stat (lnk->dst_vpath, &link_stat) == 0)) && !*ignore_all)
                    {
                        FileProgressStatus status;

                        status =
                            file_error (ctx, TRUE, _("Cannot stat hardlink source file \"%s\"\n%s"),
                                        vfs_path_as_str (lnk->dst_vpath));
                        if (status == FILE_ABORT)
                            return HARDLINK_ABORT;
                        if (status == FILE_RETRY)
                            continue;
                        if (status == FILE_IGNORE_ALL)
                            *ignore_all = TRUE;
                        break;
                    }

                    /* if stat() finished unsuccessfully, don't try to create link */
                    if (!ok)
                        return HARDLINK_ERROR;

                    while (!(ok = (mc_link (lnk->dst_vpath, dst_vpath) == 0)) && !*ignore_all)
                    {
                        FileProgressStatus status;

                        status =
                            file_error (ctx, TRUE, _("Cannot create target hardlink \"%s\"\n%s"),
                                        vfs_path_as_str (dst_vpath));
                        if (status == FILE_ABORT)
                            return HARDLINK_ABORT;
                        if (status == FILE_RETRY)
                            continue;
                        if (status == FILE_IGNORE_ALL)
                            *ignore_all = TRUE;
                        break;
                    }

                    /* Success? */
                    return (ok ? HARDLINK_OK : HARDLINK_ERROR);
                }
            }
        }

        if (!*ignore_all)
        {
            FileProgressStatus status;

            /* Message w/o "Retry" action.
             *
             * FIXME: Can't say what errno is here. Define it and don't display.
             *
             * file_error() displays a message with text representation of errno
             * and the string passed to file_error() should provide the format "%s"
             * for that at end (see previous file_error() call for the reference).
             * But if format for errno isn't provided, it is safe, because C standard says:
             * "If the format is exhausted while arguments remain, the excess arguments
             * are evaluated (as always) but are otherwise ignored" (ISO/IEC 9899:1999,
             * section 7.19.6.1, paragraph 2).
             *
             */
            errno = 0;
            status =
                file_error (ctx, FALSE, _("Cannot create target hardlink \"%s\""),
                            vfs_path_as_str (dst_vpath));

            if (status == FILE_ABORT)
                return HARDLINK_ABORT;

            if (status == FILE_IGNORE_ALL)
                *ignore_all = TRUE;
        }

        return HARDLINK_ERROR;
    }

    lnk = g_try_new (link_t, 1);
    if (lnk != NULL)
    {
        lnk->vfs = vfs_path_get_last_path_vfs (src_vpath);
        lnk->ino = ino;
        lnk->dev = dev;
        lnk->st_mode = 0;
        lnk->src_vpath = vfs_path_clone (src_vpath);
        lnk->dst_vpath = vfs_path_clone (dst_vpath);

        linklist = g_slist_prepend (linklist, lnk);
    }

    return HARDLINK_CACHED;
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Duplicate the contents of the symbolic link src_vpath in dst_vpath.
 * Try to make a stable symlink if the option "stable symlink" was
 * set in the file mask dialog.
 * If dst_path is an existing symlink it will be deleted silently
 * (upper levels take already care of existing files at dst_vpath).
 */

static FileProgressStatus
make_symlink (file_op_context_t *ctx, const vfs_path_t *src_vpath, const vfs_path_t *dst_vpath)
{
    const char *src_path;
    const char *dst_path;
    char link_target[MC_MAXPATHLEN];
    int len;
    FileProgressStatus return_status;
    mc_stat_t dst_stat;
    gboolean dst_is_symlink;
    vfs_path_t *link_target_vpath = NULL;

    src_path = vfs_path_as_str (src_vpath);
    dst_path = vfs_path_as_str (dst_vpath);

    dst_is_symlink = (mc_lstat (dst_vpath, &dst_stat) == 0) && S_ISLNK (dst_stat.st_mode);

  retry_src_readlink:
    len = mc_readlink (src_vpath, link_target, sizeof (link_target) - 1);
    if (len < 0)
    {
        if (ctx->ignore_all)
            return_status = FILE_IGNORE_ALL;
        else
        {
            return_status =
                file_error (ctx, TRUE, _("Cannot read source link \"%s\"\n%s"), src_path);
            if (return_status == FILE_IGNORE_ALL)
                ctx->ignore_all = TRUE;
            if (return_status == FILE_RETRY)
                goto retry_src_readlink;
        }
        goto ret;
    }

    link_target[len] = '\0';

    if (ctx->stable_symlinks && !(vfs_file_is_local (src_vpath) && vfs_file_is_local (dst_vpath)))
    {
        message (D_ERROR, MSG_ERROR,
                 _("Cannot make stable symlinks across "
                   "non-local filesystems:\n\nOption Stable Symlinks will be disabled"));
        ctx->stable_symlinks = FALSE;
    }

    if (ctx->stable_symlinks && !g_path_is_absolute (link_target))
    {
        const char *r;

#if defined(WIN32) //WIN32, path
        r = strrchr2 (src_path, PATH_SEP, PATH_SEP2);
#else
        r = strrchr (src_path, PATH_SEP);
#endif
        if (r != NULL)
        {
            size_t slen;
            GString *p;
            vfs_path_t *q;

            slen = r - src_path + 1;

            p = g_string_sized_new (slen + len);
            g_string_append_len (p, src_path, slen);

            if (g_path_is_absolute (dst_path))
                q = vfs_path_from_str_flags (dst_path, VPF_NO_CANON);
            else
                q = vfs_path_build_filename (p->str, dst_path, (char *) NULL);

            if (vfs_path_tokens_count (q) > 1)
            {
                char *s = NULL;
                vfs_path_t *tmp_vpath1, *tmp_vpath2;

                g_string_append_len (p, link_target, len);
                tmp_vpath1 = vfs_path_vtokens_get (q, -1, 1);
                tmp_vpath2 = vfs_path_from_str (p->str);
                s = diff_two_paths (tmp_vpath1, tmp_vpath2);
                vfs_path_free (tmp_vpath2, TRUE);
                vfs_path_free (tmp_vpath1, TRUE);
                g_strlcpy (link_target, s != NULL ? s : p->str, sizeof (link_target));
                g_free (s);
            }

            g_string_free (p, TRUE);
            vfs_path_free (q, TRUE);
        }
    }
    link_target_vpath = vfs_path_from_str_flags (link_target, VPF_NO_CANON);

  retry_dst_symlink:
    if (mc_symlink (link_target_vpath, dst_vpath) == 0)
    {
        /* Success */
        return_status = FILE_CONT;
        goto ret;
    }
    /*
     * if dst_exists, it is obvious that this had failed.
     * We can delete the old symlink and try again...
     */
    if (dst_is_symlink && mc_unlink (dst_vpath) == 0
        && mc_symlink (link_target_vpath, dst_vpath) == 0)
    {
        /* Success */
        return_status = FILE_CONT;
        goto ret;
    }

    if (ctx->ignore_all)
        return_status = FILE_IGNORE_ALL;
    else
    {
        return_status =
            file_error (ctx, TRUE, _("Cannot create target symlink \"%s\"\n%s"), dst_path);
        if (return_status == FILE_IGNORE_ALL)
            ctx->ignore_all = TRUE;
        if (return_status == FILE_RETRY)
            goto retry_dst_symlink;
    }

  ret:
    vfs_path_free (link_target_vpath, TRUE);
    return return_status;
}

/* --------------------------------------------------------------------------------------------- */
/**
 * do_compute_dir_size:
 *
 * Computes the number of bytes used by the files in a directory
 */

static FileProgressStatus
do_compute_dir_size (const vfs_path_t *dirname_vpath, dirsize_status_msg_t *dsm,
                     size_t *dir_count, size_t *ret_marked, uintmax_t *ret_total,
                     mc_stat_fn stat_func)
{
    static gint64 timestamp = 0;
    /* update with 25 FPS rate */
    static const gint64 delay = G_USEC_PER_SEC / 25;

    status_msg_t *sm = STATUS_MSG (dsm);
    int res;
    mc_stat_t s;
    DIR *dir;
    struct vfs_dirent *dirent;
    FileProgressStatus ret = FILE_CONT;

    (*dir_count)++;

    dir = mc_opendir (dirname_vpath);
    if (dir == NULL)
        return ret;

    while (ret == FILE_CONT && (dirent = mc_readdir (dir)) != NULL)
    {
        vfs_path_t *tmp_vpath;

        if (DIR_IS_DOT (dirent->d_name) || DIR_IS_DOTDOT (dirent->d_name))
            continue;

        tmp_vpath = vfs_path_append_new (dirname_vpath, dirent->d_name, (char *) NULL);

        res = stat_func (tmp_vpath, &s);
        if (res == 0)
        {
            if (S_ISDIR (s.st_mode))
                ret =
                    do_compute_dir_size (tmp_vpath, dsm, dir_count, ret_marked, ret_total,
                                         stat_func);
            else
            {
                ret = FILE_CONT;

                (*ret_marked)++;
                *ret_total += (uintmax_t) s.st_size;
            }

            if (ret == FILE_CONT && sm->update != NULL && mc_time_elapsed (&timestamp, delay))
            {
                dsm->dirname_vpath = tmp_vpath;
                dsm->dir_count = *dir_count;
                dsm->total_size = *ret_total;
                ret = sm->update (sm);
            }
        }

        vfs_path_free (tmp_vpath, TRUE);
    }

    mc_closedir (dir);
    return ret;
}

/* --------------------------------------------------------------------------------------------- */
/**
 * panel_compute_totals:
 *
 * compute the number of files and the number of bytes
 * used up by the whole selection, recursing directories
 * as required.  In addition, it checks to see if it will
 * overwrite any files by doing the copy.
 */

static FileProgressStatus
panel_compute_totals (const WPanel *panel, dirsize_status_msg_t *sm, size_t *ret_count,
                      uintmax_t *ret_total, gboolean follow_symlinks)
{
    int i;
    size_t dir_count = 0;
    mc_stat_fn stat_func = follow_symlinks ? mc_stat : mc_lstat;

    for (i = 0; i < panel->dir.len; i++)
    {
        const file_entry_t *fe = &panel->dir.list[i];
        const mc_stat_t *s;

        if (fe->f.marked == 0)
            continue;

        s = &fe->st;

        if (S_ISDIR (s->st_mode) || (follow_symlinks && link_isdir (fe) && fe->f.stale_link == 0))
        {
            vfs_path_t *p;
            FileProgressStatus status;

            p = vfs_path_append_new (panel->cwd_vpath, fe->fname->str, (char *) NULL);
            status = do_compute_dir_size (p, sm, &dir_count, ret_count, ret_total, stat_func);
            vfs_path_free (p, TRUE);

            if (status != FILE_CONT)
                return status;
        }
        else
        {
            (*ret_count)++;
            *ret_total += (uintmax_t) s->st_size;
        }
    }

    return FILE_CONT;
}

/* --------------------------------------------------------------------------------------------- */

/** Initialize variables for progress bars */
static FileProgressStatus
panel_operate_init_totals (const WPanel *panel, const vfs_path_t *source,
                           const mc_stat_t *source_stat, file_op_context_t *ctx,
                           gboolean compute_totals, filegui_dialog_type_t dialog_type)
{
    FileProgressStatus status;

#ifdef ENABLE_BACKGROUND
    if (mc_global.we_are_background)
        return FILE_CONT;
#endif

    if (verbose && compute_totals)
    {
        dirsize_status_msg_t dsm;
        gboolean stale_link = FALSE;

        memset (&dsm, 0, sizeof (dsm));
        dsm.allow_skip = TRUE;
        status_msg_init (STATUS_MSG (&dsm), _("Directory scanning"), 0, dirsize_status_init_cb,
                         dirsize_status_update_cb, dirsize_status_deinit_cb);

        ctx->total_count = 0;
        ctx->total_bytes = 0;

        if (source == NULL)
            status = panel_compute_totals (panel, &dsm, &ctx->total_count, &ctx->total_bytes,
                                           ctx->follow_links);
        else if (S_ISDIR (source_stat->st_mode)
                 || (ctx->follow_links
                     && file_is_symlink_to_dir (source, (mc_stat_t *) source_stat, &stale_link)
                     && !stale_link))
        {
            size_t dir_count = 0;

            status = do_compute_dir_size (source, &dsm, &dir_count, &ctx->total_count,
                                          &ctx->total_bytes, ctx->stat_func);
        }
        else
        {
            ctx->total_count++;
            ctx->total_bytes += (uintmax_t) source_stat->st_size;
            status = FILE_CONT;
        }

        status_msg_deinit (STATUS_MSG (&dsm));

        ctx->totals_computed = (status == FILE_CONT);

        if (status == FILE_SKIP)
            status = FILE_CONT;
    }
    else
    {
        status = FILE_CONT;
        ctx->total_count = panel->marked;
        ctx->total_bytes = panel->total;
        ctx->totals_computed = verbose && dialog_type == FILEGUI_DIALOG_ONE_ITEM;
    }

    /* destroy already created UI for single file rename operation */
    file_progress_ui_destroy (ctx);

    file_progress_ui_create (ctx, TRUE, dialog_type);

    return status;
}

/* --------------------------------------------------------------------------------------------- */

static void
progress_update_one (gboolean success, file_op_context_t *ctx, mc_off_t add)
{
    gint64 tv_current;
    static gint64 tv_start = -1;

    ctx->total_progress_count++;
    ctx->total_progress_bytes += (uintmax_t) add;

    if (!success)
        return;

    tv_current = g_get_monotonic_time ();

    if (tv_start < 0)
        tv_start = tv_current;
    else if (tv_current - tv_start > FILEOP_UPDATE_INTERVAL_US)
    {
        if (verbose && ctx->dialog_type == FILEGUI_DIALOG_MULTI_ITEM)
        {
            file_progress_show_count (ctx);
            file_progress_show_total (ctx, ctx->total_progress_bytes, tv_current, TRUE);
        }

        tv_start = tv_current;
    }
}

/* --------------------------------------------------------------------------------------------- */

static FileProgressStatus
real_warn_same_file (file_op_context_t *ctx, enum OperationMode mode, const char *fmt,
                     const char *a, const char *b)
{
    char *msg;
    int result = 0;
    const char *head_msg;
    int width_a, width_b, width;

    const gint64 t = g_get_monotonic_time ();

    head_msg = mode == Foreground ? MSG_ERROR : _("Background process error");

    width_a = str_term_width1 (a);
    width_b = str_term_width1 (b);
    width = COLS - 8;

    if (width_a > width)
    {
        if (width_b > width)
        {
            char *s;

            s = g_strndup (str_trunc (a, width), width);
            b = str_trunc (b, width);
            msg = g_strdup_printf (fmt, s, b);
            g_free (s);
        }
        else
        {
            a = str_trunc (a, width);
            msg = g_strdup_printf (fmt, a, b);
        }
    }
    else
    {
        if (width_b > width)
            b = str_trunc (b, width);

        msg = g_strdup_printf (fmt, a, b);
    }

    result = query_dialog (head_msg, msg, D_ERROR, 2, _("&Skip"), _("&Abort"));
    g_free (msg);
    do_refresh ();

    ctx->pauses += g_get_monotonic_time () - t;

    return (result == 1) ? FILE_ABORT : FILE_SKIP;
}

/* --------------------------------------------------------------------------------------------- */

static FileProgressStatus
warn_same_file (file_op_context_t *ctx, const char *fmt, const char *a, const char *b)
{
#ifdef ENABLE_BACKGROUND
/* *INDENT-OFF* */
    union
    {
        void *p;
        FileProgressStatus (*f) (file_op_context_t *ctx, enum OperationMode, const char *fmt,
                                 const char *a, const char *b);
    } pntr;
/* *INDENT-ON* */

    pntr.f = real_warn_same_file;

    if (mc_global.we_are_background)
        return parent_call (pntr.p, ctx, 3, strlen (fmt), fmt, strlen (a), a, strlen (b), b);
#endif
    return real_warn_same_file (ctx, Foreground, fmt, a, b);
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
check_same_file (file_op_context_t *ctx, const char *a, const mc_stat_t *ast, const char *b,
                 const mc_stat_t *bst, FileProgressStatus *status)
{
    if (ast->st_dev != bst->st_dev || ast->st_ino != bst->st_ino)
        return FALSE;

    if (S_ISDIR (ast->st_mode))
        *status = warn_same_file (ctx, _("\"%s\"\nand\n\"%s\"\nare the same directory"), a, b);
    else
        *status = warn_same_file (ctx, _("\"%s\"\nand\n\"%s\"\nare the same file"), a, b);

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */
/* {{{ Query/status report routines */

static FileProgressStatus
real_do_file_error (file_op_context_t *ctx, enum OperationMode mode, gboolean allow_retry,
                    const char *error)
{
    gint64 t = 0;
    int result;
    const char *msg;

    if (ctx != NULL)
        t = g_get_monotonic_time ();

    msg = mode == Foreground ? MSG_ERROR : _("Background process error");

    if (allow_retry)
        result =
            query_dialog (msg, error, D_ERROR, 4, _("&Ignore"), _("Ignore a&ll"), _("&Retry"),
                          _("&Abort"));
    else
        result = query_dialog (msg, error, D_ERROR, 3, _("&Ignore"), _("Ignore a&ll"), _("&Abort"));

    if (ctx != NULL)
        ctx->pauses += g_get_monotonic_time () - t;

    switch (result)
    {
    case 0:
        do_refresh ();
        return FILE_IGNORE;

    case 1:
        do_refresh ();
        return FILE_IGNORE_ALL;

    case 2:
        if (allow_retry)
        {
            do_refresh ();
            return FILE_RETRY;
        }
        MC_FALLTHROUGH;

    case 3:
    default:
        return FILE_ABORT;
    }
}

/* --------------------------------------------------------------------------------------------- */

static FileProgressStatus
real_query_recursive (file_op_context_t *ctx, enum OperationMode mode, const char *s)
{
    if (ctx->recursive_result < RECURSIVE_ALWAYS)
    {
        const char *msg;
        char *text;

        const gint64 t = g_get_monotonic_time ();

        msg = mode == Foreground
            ? _("Directory \"%s\" not empty.\nDelete it recursively?")
            : _("Background process:\nDirectory \"%s\" not empty.\nDelete it recursively?");
        text = g_strdup_printf (msg, path_trunc (s, 30));

        if (safe_delete)
            query_set_sel (1);

        ctx->recursive_result =
            query_dialog (op_names[OP_DELETE], text, D_ERROR, 5, _("&Yes"), _("&No"), _("A&ll"),
                          _("Non&e"), _("&Abort"));
        g_free (text);

        if (ctx->recursive_result != RECURSIVE_ABORT)
            do_refresh ();

        ctx->pauses += g_get_monotonic_time () - t;
    }

    switch (ctx->recursive_result)
    {
    case RECURSIVE_YES:
    case RECURSIVE_ALWAYS:
        return FILE_CONT;

    case RECURSIVE_NO:
    case RECURSIVE_NEVER:
        return FILE_SKIP;

    case RECURSIVE_ABORT:
    default:
        return FILE_ABORT;
    }
}

/* --------------------------------------------------------------------------------------------- */

#ifdef ENABLE_BACKGROUND
static FileProgressStatus
do_file_error (file_op_context_t *ctx, gboolean allow_retry, const char *str)
{
/* *INDENT-OFF* */
    union
    {
        void *p;
        FileProgressStatus (*f) (file_op_context_t *ctx, enum OperationMode, gboolean, const char *);
    } pntr;
/* *INDENT-ON* */

    pntr.f = real_do_file_error;

    if (mc_global.we_are_background)
        return parent_call (pntr.p, ctx, 2, sizeof (allow_retry), allow_retry, strlen (str), str);
    else
        return real_do_file_error (ctx, Foreground, allow_retry, str);
}

/* --------------------------------------------------------------------------------------------- */

static FileProgressStatus
query_recursive (file_op_context_t *ctx, const char *s)
{
/* *INDENT-OFF* */
    union
    {
        void *p;
        FileProgressStatus (*f) (file_op_context_t *, enum OperationMode, const char *);
    } pntr;
/* *INDENT-ON* */

    pntr.f = real_query_recursive;

    if (mc_global.we_are_background)
        return parent_call (pntr.p, ctx, 1, strlen (s), s);
    else
        return real_query_recursive (ctx, Foreground, s);
}

/* --------------------------------------------------------------------------------------------- */

static FileProgressStatus
query_replace (file_op_context_t *ctx, const char *src, mc_stat_t *src_stat, const char *dst,
               mc_stat_t *dst_stat)
{
/* *INDENT-OFF* */
    union
    {
        void *p;
        FileProgressStatus (*f) (file_op_context_t *, enum OperationMode, const char *,
                                 mc_stat_t *, const char *, mc_stat_t *);
    } pntr;
/* *INDENT-ON* */

    pntr.f = file_progress_real_query_replace;

    if (mc_global.we_are_background)
        return parent_call (pntr.p, ctx, 4, strlen (src), src, sizeof (mc_stat_t), src_stat,
                            strlen (dst), dst, sizeof (mc_stat_t), dst_stat);
    else
        return file_progress_real_query_replace (ctx, Foreground, src, src_stat, dst, dst_stat);
}

#else
/* --------------------------------------------------------------------------------------------- */

static FileProgressStatus
do_file_error (file_op_context_t *ctx, gboolean allow_retry, const char *str)
{
    return real_do_file_error (ctx, Foreground, allow_retry, str);
}

/* --------------------------------------------------------------------------------------------- */

static FileProgressStatus
query_recursive (file_op_context_t *ctx, const char *s)
{
    return real_query_recursive (ctx, Foreground, s);
}

/* --------------------------------------------------------------------------------------------- */

static FileProgressStatus
query_replace (file_op_context_t *ctx, const char *src, mc_stat_t *src_stat, const char *dst,
               mc_stat_t *dst_stat)
{
    return file_progress_real_query_replace (ctx, Foreground, src, src_stat, dst, dst_stat);
}

#endif /* !ENABLE_BACKGROUND */

/* --------------------------------------------------------------------------------------------- */
/** Report error with two files */

static FileProgressStatus
files_error (file_op_context_t *ctx, const char *format, const char *file1, const char *file2)
{
    char buf[BUF_MEDIUM];
    char *nfile1, *nfile2;

    nfile1 = g_strdup (path_trunc (file1, 15));
    nfile2 = g_strdup (path_trunc (file2, 15));
    g_snprintf (buf, sizeof (buf), format, nfile1, nfile2, unix_error_string (errno));
    g_free (nfile1);
    g_free (nfile2);

    return do_file_error (ctx, TRUE, buf);
}

/* }}} */

/* --------------------------------------------------------------------------------------------- */

static void
calc_copy_file_progress (file_op_context_t *ctx, gint64 tv_current, mc_off_t file_part,
                         mc_off_t file_size)
{
    double dt;

    /* Update rotating dash after some time */
    rotate_dash (TRUE);

    /* Compute ETA */
    dt = (tv_current - ctx->pauses - ctx->transfer_start) / (double) G_USEC_PER_SEC;

    if (file_part == 0)
        ctx->eta_secs = 0.0;
    else
        ctx->eta_secs = ((double) file_size / file_part - 1) * dt;

    /* Compute BPS rate */
    dt = MAX (1.0, dt);
    ctx->bps = (long) (file_part / dt);

    /* Compute total ETA and BPS */
    if (ctx->total_bytes != 0)
    {
        dt = (tv_current - ctx->pauses - ctx->total_transfer_start) / (double) G_USEC_PER_SEC;

        const uintmax_t copied_bytes = ctx->total_progress_bytes + file_part;
        if (copied_bytes == 0)
            ctx->total_eta_secs = 0;
        else
            ctx->total_eta_secs = ((double) ctx->total_bytes / copied_bytes - 1) * dt;

        dt = MAX (1.0, dt);
        ctx->total_bps = (long) (copied_bytes / dt);
    }
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
try_remove_file (file_op_context_t *ctx, const vfs_path_t *vpath, FileProgressStatus *status)
{
    while (mc_unlink (vpath) != 0 && !ctx->ignore_all)
    {
        *status =
            file_error (ctx, TRUE, _("Cannot remove file \"%s\"\n%s"), vfs_path_as_str (vpath));
        if (*status == FILE_RETRY)
            continue;
        if (*status == FILE_IGNORE_ALL)
            ctx->ignore_all = TRUE;
        return FALSE;
    }

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

/* {{{ Move routines */

/**
 * Move single file or one of many files from one location to another.
 *
 * @panel pointer to panel in case of single file, NULL otherwise
 * @ctx file operation context object
 * @s source file name
 * @d destination file name
 *
 * @return operation result
 */
static FileProgressStatus
move_file_file (const WPanel *panel, file_op_context_t *ctx, const char *s, const char *d)
{
    mc_stat_t src_stat, dst_stat;
    FileProgressStatus return_status = FILE_CONT;
    gboolean copy_done = FALSE;
    gboolean old_ask_overwrite;
    vfs_path_t *src_vpath, *dst_vpath;

    src_vpath = vfs_path_from_str (s);
    dst_vpath = vfs_path_from_str (d);

    file_progress_show_source (ctx, src_vpath);
    file_progress_show_target (ctx, dst_vpath);

    /* FIXME: do we really need to check buttons in case of single file? */
    if (file_progress_check_buttons (ctx) == FILE_ABORT)
    {
        return_status = FILE_ABORT;
        goto ret_fast;
    }

    mc_refresh ();

    while (mc_lstat (src_vpath, &src_stat) != 0)
    {
        /* Source doesn't exist */
        if (ctx->ignore_all)
            return_status = FILE_IGNORE_ALL;
        else
        {
            return_status = file_error (ctx, TRUE, _("Cannot stat file \"%s\"\n%s"), s);
            if (return_status == FILE_IGNORE_ALL)
                ctx->ignore_all = TRUE;
        }

        if (return_status != FILE_RETRY)
            goto ret;
    }

    if (mc_lstat (dst_vpath, &dst_stat) == 0)
    {
        if (check_same_file (ctx, s, &src_stat, d, &dst_stat, &return_status))
            goto ret;

        if (S_ISDIR (dst_stat.st_mode))
        {
            message (D_ERROR, MSG_ERROR, _("Cannot overwrite directory \"%s\""), d);
            do_refresh ();
            return_status = FILE_SKIP;
            goto ret;
        }

        if (confirm_overwrite)
        {
            return_status = query_replace (ctx, s, &src_stat, d, &dst_stat);
            if (return_status != FILE_CONT)
                goto ret;
        }
        /* Ok to overwrite */
    }

    if (!ctx->do_append)
    {
        if (S_ISLNK (src_stat.st_mode) && ctx->stable_symlinks)
        {
            return_status = make_symlink (ctx, src_vpath, dst_vpath);
            if (return_status == FILE_CONT)
            {
                if (ctx->preserve)
                {
                    mc_timesbuf_t times;

                    vfs_get_timesbuf_from_stat (&src_stat, &times);
                    mc_utime (dst_vpath, &times);
                }
                goto retry_src_remove;
            }
            goto ret;
        }

        if (mc_rename (src_vpath, dst_vpath) == 0)
            goto ret;
    }
#if 0
    /* Comparison to EXDEV seems not to work in nfs if you're moving from
       one nfs to the same, but on the server it is on two different
       filesystems. Then nfs returns EIO instead of EXDEV.
       Hope it will not hurt if we always in case of error try to copy/delete. */
    else
        errno = EXDEV;          /* Hack to copy (append) the file and then delete it */

    if (errno != EXDEV)
    {
        if (ctx->ignore_all)
            return_status = FILE_IGNORE_ALL;
        else
        {
            return_status = files_error (ctx, _("Cannot move file \"%s\" to \"%s\"\n%s"), s, d);
            if (return_status == FILE_IGNORE_ALL)
                ctx->ignore_all = TRUE;
            if (return_status == FILE_RETRY)
                goto retry_rename;
        }

        goto ret;
    }
#endif

    /* Failed rename -> copy the file instead */
    if (panel != NULL)
    {
        /* In case of single file, calculate totals. In case of many files,
           totals are calculated already. */
        return_status =
            panel_operate_init_totals (panel, src_vpath, &src_stat, ctx, TRUE,
                                       FILEGUI_DIALOG_ONE_ITEM);
        if (return_status != FILE_CONT)
            goto ret_fast;
    }

    old_ask_overwrite = ctx->ask_overwrite;
    ctx->ask_overwrite = FALSE;
    return_status = copy_file_file (ctx, s, d);
    ctx->ask_overwrite = old_ask_overwrite;
    if (return_status != FILE_CONT)
        goto ret;

    copy_done = TRUE;

    /* FIXME: there is no need to update progress and check buttons
       at the finish of single file operation. */
    if (panel == NULL)
    {
        file_progress_show_source (ctx, NULL);
        if (verbose)
            file_progress_show (ctx, 0, 0, "", FALSE);

        return_status = file_progress_check_buttons (ctx);
        if (return_status != FILE_CONT)
            goto ret_fast;
    }

    mc_refresh ();

  retry_src_remove:
    if (!try_remove_file (ctx, src_vpath, &return_status) && panel == NULL)
        goto ret_fast;

  ret:
    if (return_status != FILE_ABORT)
    {
        /* if copy_done == TRUE, progress_update_one() was called in copy_file_file() */
        if (!copy_done)
            progress_update_one (TRUE, ctx, src_stat.st_size);
        return_status = file_progress_check_buttons (ctx);
    }
  ret_fast:
    vfs_path_free (src_vpath, TRUE);
    vfs_path_free (dst_vpath, TRUE);

    return return_status;
}

/* }}} */

/* --------------------------------------------------------------------------------------------- */
/* {{{ Erase routines */
/** Don't update progress status if progress_count==NULL */

static FileProgressStatus
erase_file (file_op_context_t *ctx, const vfs_path_t *vpath)
{
    mc_stat_t buf;
    FileProgressStatus return_status;

    /* check buttons if deleting info was changed */
    if (file_progress_show_deleting (ctx, vpath, &ctx->total_progress_count))
    {
        file_progress_show_count (ctx);
        if (file_progress_check_buttons (ctx) == FILE_ABORT)
            return FILE_ABORT;

        mc_refresh ();
    }

    if (ctx->total_progress_count != 0 && mc_lstat (vpath, &buf) != 0)
    {
        /* ignore, most likely the mc_unlink fails, too */
        buf.st_size = 0;
    }

    if (!try_remove_file (ctx, vpath, &return_status) && return_status == FILE_ABORT)
        return FILE_ABORT;

    if (ctx->total_progress_count == 0)
        return FILE_CONT;

    return file_progress_check_buttons (ctx);
}

/* --------------------------------------------------------------------------------------------- */

static FileProgressStatus
try_erase_dir (file_op_context_t *ctx, const vfs_path_t *vpath)
{
    const char *dir;
    FileProgressStatus return_status = FILE_CONT;

    dir = vfs_path_as_str (vpath);

    while (my_rmdir (dir) != 0 && !ctx->ignore_all)
    {
        return_status = file_error (ctx, TRUE, _("Cannot remove directory \"%s\"\n%s"), dir);
        if (return_status == FILE_IGNORE_ALL)
            ctx->ignore_all = TRUE;
        if (return_status != FILE_RETRY)
            break;
    }

    return return_status;
}

/* --------------------------------------------------------------------------------------------- */

/**
  Recursive removal of files
  abort -> cancel stack
  ignore -> warn every level, gets default
  ignore_all -> remove as much as possible
*/
static FileProgressStatus
recursive_erase (file_op_context_t *ctx, const vfs_path_t *vpath)
{
    struct vfs_dirent *next;
    DIR *reading;
    FileProgressStatus return_status = FILE_CONT;

    reading = mc_opendir (vpath);
    if (reading == NULL)
        return FILE_RETRY;

    while ((next = mc_readdir (reading)) && return_status != FILE_ABORT)
    {
        vfs_path_t *tmp_vpath;
        mc_stat_t buf;

        if (DIR_IS_DOT (next->d_name) || DIR_IS_DOTDOT (next->d_name))
            continue;

        tmp_vpath = vfs_path_append_new (vpath, next->d_name, (char *) NULL);
        if (mc_lstat (tmp_vpath, &buf) != 0)
        {
            mc_closedir (reading);
            vfs_path_free (tmp_vpath, TRUE);
            return FILE_RETRY;
        }
        if (S_ISDIR (buf.st_mode))
            return_status = recursive_erase (ctx, tmp_vpath);
        else
            return_status = erase_file (ctx, tmp_vpath);
        vfs_path_free (tmp_vpath, TRUE);
    }
    mc_closedir (reading);

    if (return_status == FILE_ABORT)
        return FILE_ABORT;

    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 ();

    return try_erase_dir (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
  *
  * @returns -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 \"%s\"\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
erase_dir_after_copy (file_op_context_t *ctx, const vfs_path_t *vpath, FileProgressStatus *status)
{
    if (ctx->erase_at_end && erase_list != NULL)
    {
        /* Reset progress count before delete to avoid counting files twice */
        ctx->total_progress_count = ctx->prev_total_progress_count;

        while (!g_queue_is_empty (erase_list) && *status != FILE_ABORT)
        {
            link_t *lp;

            lp = (link_t *) g_queue_pop_head (erase_list);

            if (S_ISDIR (lp->st_mode))
                *status = erase_dir_iff_empty (ctx, lp->src_vpath);
            else
                *status = erase_file (ctx, lp->src_vpath);

            free_link (lp);
        }

        /* Save progress counter before move next directory */
        ctx->prev_total_progress_count = ctx->total_progress_count;
    }

    erase_dir_iff_empty (ctx, vpath);
}

/* }}} */

/* --------------------------------------------------------------------------------------------- */

/**
 * Move single directory or one of many directories from one location to another.
 *
 * @panel pointer to panel in case of single directory, NULL otherwise
 * @ctx file operation context object
 * @s source directory name
 * @d destination directory name
 *
 * @return operation result
 */
static FileProgressStatus
do_move_dir_dir (const WPanel *panel, file_op_context_t *ctx, const char *s, const char *d)
{
    mc_stat_t src_stat, dst_stat;
    FileProgressStatus return_status = FILE_CONT;
    gboolean move_over = FALSE;
    gboolean dstat_ok;
    vfs_path_t *src_vpath, *dst_vpath;

    src_vpath = vfs_path_from_str (s);
    dst_vpath = vfs_path_from_str (d);

    file_progress_show_source (ctx, src_vpath);
    file_progress_show_target (ctx, dst_vpath);

    /* FIXME: do we really need to check buttons in case of single directory? */
    if (panel != NULL && file_progress_check_buttons (ctx) == FILE_ABORT)
    {
        return_status = FILE_ABORT;
        goto ret_fast;
    }

    mc_refresh ();

    mc_stat (src_vpath, &src_stat);

    dstat_ok = (mc_stat (dst_vpath, &dst_stat) == 0);

    if (dstat_ok && check_same_file (ctx, s, &src_stat, d, &dst_stat, &return_status))
        goto ret_fast;

    if (!dstat_ok)
        ;                       /* destination doesn't exist */
    else if (!ctx->dive_into_subdirs)
        move_over = TRUE;
    else
    {
        vfs_path_t *tmp;

        tmp = dst_vpath;
        dst_vpath = vfs_path_append_new (dst_vpath, x_basename (s), (char *) NULL);
        vfs_path_free (tmp, TRUE);
    }

    d = vfs_path_as_str (dst_vpath);

    /* Check if the user inputted an existing dir */
  retry_dst_stat:
    if (mc_stat (dst_vpath, &dst_stat) == 0)
    {
        if (move_over)
        {
            if (panel != NULL)
            {
                /* In case of single directory, calculate totals. In case of many directories,
                   totals are calculated already. */
                return_status =
                    panel_operate_init_totals (panel, src_vpath, &src_stat, ctx, TRUE,
                                               FILEGUI_DIALOG_MULTI_ITEM);
                if (return_status != FILE_CONT)
                    goto ret;
            }

            return_status = copy_dir_dir (ctx, s, d, FALSE, TRUE, TRUE, NULL);

            if (return_status != FILE_CONT)
                goto ret;
            goto oktoret;
        }
        else if (ctx->ignore_all)
            return_status = FILE_IGNORE_ALL;
        else
        {
            if (S_ISDIR (dst_stat.st_mode))
                return_status =
                    file_error (ctx, TRUE, _("Cannot overwrite directory \"%s\"\n%s"), d);
            else
                return_status = file_error (ctx, TRUE, _("Cannot overwrite file \"%s\"\n%s"), d);
            if (return_status == FILE_IGNORE_ALL)
                ctx->ignore_all = TRUE;
            if (return_status == FILE_RETRY)
                goto retry_dst_stat;
        }

        goto ret_fast;
    }

  retry_rename:
    if (mc_rename (src_vpath, dst_vpath) == 0)
    {
        return_status = FILE_CONT;
        goto ret;
    }

    if (errno != EXDEV)
    {
        if (!ctx->ignore_all)
        {
            return_status =
                files_error (ctx, _("Cannot move directory \"%s\" to \"%s\"\n%s"), s, d);
            if (return_status == FILE_IGNORE_ALL)
                ctx->ignore_all = TRUE;
            if (return_status == FILE_RETRY)
                goto retry_rename;
        }
        goto ret;
    }

    /* Failed because of filesystem boundary -> copy dir instead */
    if (panel != NULL)
    {
        /* In case of single directory, calculate totals. In case of many directories,
           totals are calculated already. */
        return_status =
            panel_operate_init_totals (panel, src_vpath, &src_stat, ctx, TRUE,
                                       FILEGUI_DIALOG_MULTI_ITEM);
        if (return_status != FILE_CONT)
            goto ret;
    }

    return_status = copy_dir_dir (ctx, s, d, FALSE, FALSE, TRUE, NULL);

    if (return_status != FILE_CONT)
        goto ret;

  oktoret:
    /* FIXME: there is no need to update progress and check buttons
       at the finish of single directory operation. */
    if (panel == NULL)
    {
        file_progress_show_source (ctx, NULL);
        file_progress_show_target (ctx, NULL);
        if (verbose)
            file_progress_show (ctx, 0, 0, "", FALSE);

        return_status = file_progress_check_buttons (ctx);
        if (return_status != FILE_CONT)
            goto ret;
    }

    mc_refresh ();

    erase_dir_after_copy (ctx, src_vpath, &return_status);

  ret:
    erase_list = free_erase_list (erase_list);
  ret_fast:
    vfs_path_free (src_vpath, TRUE);
    vfs_path_free (dst_vpath, TRUE);
    return return_status;
}

/* --------------------------------------------------------------------------------------------- */

/* {{{ Panel operate routines */

/**
 * Return currently selected entry name or the name of the first marked
 * entry if there is one.
 */

static const char *
panel_get_file (const WPanel *panel)
{
    const file_entry_t *fe;

    if (get_current_type () == view_tree)
    {
        WTree *tree;
        const vfs_path_t *selected_name;

        tree = (WTree *) get_panel_widget (get_current_index ());
        selected_name = tree_selected_name (tree);
        return vfs_path_as_str (selected_name);
    }

    if (panel->marked != 0)
    {
        int i;

        for (i = 0; i < panel->dir.len; i++)
            if (panel->dir.list[i].f.marked != 0)
                return panel->dir.list[i].fname->str;
    }

    fe = panel_current_entry (panel);

    return (fe == NULL ? NULL : fe->fname->str);
}

/* --------------------------------------------------------------------------------------------- */

static const char *
check_single_entry (const WPanel *panel, gboolean force_single, mc_stat_t *src_stat)
{
    const char *source;
    gboolean ok;

    if (force_single)
    {
        const file_entry_t *fe;

        fe = panel_current_entry (panel);
        source = fe == NULL ? NULL : fe->fname->str;
    }
    else
        source = panel_get_file (panel);

    if (source == NULL)
        return NULL;

    ok = !DIR_IS_DOTDOT (source);

    if (!ok)
        message (D_ERROR, MSG_ERROR, _("Cannot operate on \"..\"!"));
    else
    {
        vfs_path_t *source_vpath;

        source_vpath = vfs_path_from_str (source);

        /* Update stat to get actual info */
        ok = mc_lstat (source_vpath, src_stat) == 0;
        if (!ok)
        {
            message (D_ERROR, MSG_ERROR, _("Cannot stat \"%s\"\n%s"),
                     path_trunc (source, 30), unix_error_string (errno));

            /* Directory was changed outside MC. Reload it forced */
            if (!panel->is_panelized)
            {
                panel_update_flags_t flags = UP_RELOAD;

                /* don't update panelized panel */
                if (get_other_type () == view_listing && other_panel->is_panelized)
                    flags |= UP_ONLY_CURRENT;

                update_panels (flags, UP_KEEPSEL);
            }
        }

        vfs_path_free (source_vpath, TRUE);
    }

    return ok ? source : NULL;
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Generate user prompt for panel operation.
 * src_stat must be not NULL for single source, and NULL for multiple sources
 */

static char *
panel_operate_generate_prompt (const WPanel *panel, FileOperation operation,
                               const mc_stat_t *src_stat)
{
    char *sp;
    char *format_string;
    const char *cp;

    static gboolean i18n_flag = FALSE;
    if (!i18n_flag)
    {
        size_t i;

        for (i = G_N_ELEMENTS (op_names1); i-- != 0;)
            op_names1[i] = Q_ (op_names1[i]);

#ifdef ENABLE_NLS
        for (i = G_N_ELEMENTS (prompt_parts); i-- != 0;)
            prompt_parts[i] = _(prompt_parts[i]);

        one_format = _(one_format);
        many_format = _(many_format);
#endif /* ENABLE_NLS */
        i18n_flag = TRUE;
    }

    /* Possible prompts:
     *   OP_COPY:
     *       "Copy file \"%s\" with source mask:"
     *       "Copy %d files with source mask:"
     *       "Copy directory \"%s\" with source mask:"
     *       "Copy %d directories with source mask:"
     *       "Copy %d files/directories with source mask:"
     *   OP_MOVE:
     *       "Move file \"%s\" with source mask:"
     *       "Move %d files with source mask:"
     *       "Move directory \"%s\" with source mask:"
     *       "Move %d directories with source mask:"
     *       "Move %d files/directories with source mask:"
     *   OP_DELETE:
     *       "Delete file \"%s\"?"
     *       "Delete %d files?"
     *       "Delete directory \"%s\"?"
     *       "Delete %d directories?"
     *       "Delete %d files/directories?"
     */

    cp = (src_stat != NULL ? one_format : many_format);

    /* 1. Substitute %o */
    format_string = str_replace_all (cp, "%o", op_names1[(int) operation]);

    /* 2. Substitute %n */
    cp = operation == OP_DELETE ? "\n" : " ";
    sp = format_string;
    format_string = str_replace_all (sp, "%n", cp);
    g_free (sp);

    /* 3. Substitute %f */
    if (src_stat != NULL)
        cp = S_ISDIR (src_stat->st_mode) ? prompt_parts[2] : prompt_parts[0];
    else if (panel->marked == panel->dirs_marked)
        cp = prompt_parts[3];
    else
        cp = panel->dirs_marked != 0 ? prompt_parts[4] : prompt_parts[1];

    sp = format_string;
    format_string = str_replace_all (sp, "%f", cp);
    g_free (sp);

    /* 4. Substitute %m */
    cp = operation == OP_DELETE ? "?" : prompt_parts[5];
    sp = format_string;
    format_string = str_replace_all (sp, "%m", cp);
    g_free (sp);

    return format_string;
}

/* --------------------------------------------------------------------------------------------- */

static char *
do_confirm_copy_move (const WPanel *panel, gboolean force_single, const char *source,
                      mc_stat_t *src_stat, file_op_context_t *ctx, gboolean *do_bg)
{
    const char *tmp_dest_dir;
    char *dest_dir;
    char *format;
    char *ret;

    /* Forced single operations default to the original name */
    if (force_single)
        tmp_dest_dir = source;
    else if (get_other_type () == view_listing)
        tmp_dest_dir = vfs_path_as_str (other_panel->cwd_vpath);
    else
        tmp_dest_dir = vfs_path_as_str (panel->cwd_vpath);

    /*
     * Add trailing backslash only when do non-local ops.
     * It saves user from occasional file renames (when destination
     * dir is deleted)
     */
    if (!force_single && tmp_dest_dir != NULL && tmp_dest_dir[0] != '\0'
        && !IS_PATH_SEP (tmp_dest_dir[strlen (tmp_dest_dir) - 1]))
    {
        /* add trailing separator */
        dest_dir = g_strconcat (tmp_dest_dir, PATH_SEP_STR, (char *) NULL);
    }
    else
    {
        /* just copy */
        dest_dir = g_strdup (tmp_dest_dir);
    }

    if (dest_dir == NULL)
        return NULL;

    if (source == NULL)
        src_stat = NULL;

    /* Generate confirmation prompt */
    format = panel_operate_generate_prompt (panel, ctx->operation, src_stat);

    ret = file_mask_dialog (ctx, source != NULL, format,
                            source != NULL ? source : (const void *) &panel->marked, dest_dir,
                            do_bg);

    g_free (format);
    g_free (dest_dir);

    return ret;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
do_confirm_erase (const WPanel *panel, const char *source, mc_stat_t *src_stat)
{
    int i;
    char *format;
    char fmd_buf[BUF_MEDIUM];

    if (source == NULL)
        src_stat = NULL;

    /* Generate confirmation prompt */
    format = panel_operate_generate_prompt (panel, OP_DELETE, src_stat);

    if (source == NULL)
        g_snprintf (fmd_buf, sizeof (fmd_buf), format, panel->marked);
    else
    {
        const int fmd_xlen = 64;

        i = fmd_xlen - str_term_width1 (format) - 4;
        g_snprintf (fmd_buf, sizeof (fmd_buf), format, str_trunc (source, i));
    }

    g_free (format);

    if (safe_delete)
        query_set_sel (1);

    i = query_dialog (op_names[OP_DELETE], fmd_buf, D_ERROR, 2, _("&Yes"), _("&No"));

    return (i == 0);
}

/* --------------------------------------------------------------------------------------------- */

static FileProgressStatus
operate_single_file (const WPanel *panel, file_op_context_t *ctx, const char *src,
                     mc_stat_t *src_stat, const char *dest, filegui_dialog_type_t dialog_type)
{
    FileProgressStatus value;
    vfs_path_t *src_vpath;
    gboolean is_file;

    if (g_path_is_absolute (src))
        src_vpath = vfs_path_from_str (src);
    else
        src_vpath = vfs_path_append_new (panel->cwd_vpath, src, (char *) NULL);

    is_file = !S_ISDIR (src_stat->st_mode);
    /* Is link to directory? */
    if (is_file)
    {
        gboolean is_link;

        is_link = file_is_symlink_to_dir (src_vpath, src_stat, NULL);
        is_file = !(is_link && ctx->follow_links);
    }

    if (ctx->operation == OP_DELETE)
    {
        value = panel_operate_init_totals (panel, src_vpath, src_stat, ctx, !is_file, dialog_type);
        if (value == FILE_CONT)
        {
            if (is_file)
                value = erase_file (ctx, src_vpath);
            else
                value = erase_dir (ctx, src_vpath);
        }
    }
    else
    {
        char *temp;

        src = vfs_path_as_str (src_vpath);

        temp = build_dest (ctx, src, dest, &value);
        if (temp != NULL)
        {
            dest = temp;

            switch (ctx->operation)
            {
            case OP_COPY:
                /* we use file_mask_op_follow_links only with OP_COPY */
                ctx->stat_func (src_vpath, src_stat);

                value =
                    panel_operate_init_totals (panel, src_vpath, src_stat, ctx, !is_file,
                                               dialog_type);
                if (value == FILE_CONT)
                {
                    is_file = !S_ISDIR (src_stat->st_mode);
                    /* Is link to directory? */
                    if (is_file)
                    {
                        gboolean is_link;

                        is_link = file_is_symlink_to_dir (src_vpath, src_stat, NULL);
                        is_file = !(is_link && ctx->follow_links);
                    }

                    if (is_file)
                        value = copy_file_file (ctx, src, dest);
                    else
                        value = copy_dir_dir (ctx, src, dest, TRUE, FALSE, FALSE, NULL);
                }
                break;

            case OP_MOVE:
#ifdef ENABLE_BACKGROUND
                if (!mc_global.we_are_background)
#endif
                    /* create UI to show confirmation dialog */
                    file_progress_ui_create (ctx, TRUE, FILEGUI_DIALOG_ONE_ITEM);

                if (is_file)
                    value = move_file_file (panel, ctx, src, dest);
                else
                    value = do_move_dir_dir (panel, ctx, src, dest);
                break;

            default:
                /* Unknown file operation */
                abort ();
            }

            g_free (temp);
        }
    }

    vfs_path_free (src_vpath, TRUE);

    return value;
}

/* --------------------------------------------------------------------------------------------- */

static FileProgressStatus
operate_one_file (const WPanel *panel, file_op_context_t *ctx, const char *src,
                  mc_stat_t *src_stat, const char *dest)
{
    FileProgressStatus value = FILE_CONT;
    vfs_path_t *src_vpath;
    gboolean is_file;

    if (g_path_is_absolute (src))
        src_vpath = vfs_path_from_str (src);
    else
        src_vpath = vfs_path_append_new (panel->cwd_vpath, src, (char *) NULL);

    is_file = !S_ISDIR (src_stat->st_mode);

    if (ctx->operation == OP_DELETE)
    {
        if (is_file)
            value = erase_file (ctx, src_vpath);
        else
            value = erase_dir (ctx, src_vpath);
    }
    else
    {
        char *temp;

        src = vfs_path_as_str (src_vpath);

        temp = build_dest (ctx, src, dest, &value);
        if (temp != NULL)
        {
            dest = temp;

            switch (ctx->operation)
            {
            case OP_COPY:
                /* we use file_mask_op_follow_links only with OP_COPY */
                ctx->stat_func (src_vpath, src_stat);
                is_file = !S_ISDIR (src_stat->st_mode);

                if (is_file)
                    value = copy_file_file (ctx, src, dest);
                else
                    value = copy_dir_dir (ctx, src, dest, TRUE, FALSE, FALSE, NULL);
                dest_dirs = free_linklist (dest_dirs);
                break;

            case OP_MOVE:
                if (is_file)
                    value = move_file_file (NULL, ctx, src, dest);
                else
                    value = do_move_dir_dir (NULL, ctx, src, dest);
                break;

            default:
                /* Unknown file operation */
                abort ();
            }

            g_free (temp);
        }
    }

    vfs_path_free (src_vpath, TRUE);

    return value;
}

/* --------------------------------------------------------------------------------------------- */

#ifdef ENABLE_BACKGROUND
static int
end_bg_process (file_op_context_t *ctx, enum OperationMode mode)
{
    int pid = ctx->pid;

    (void) mode;
    ctx->pid = 0;

    unregister_task_with_pid (pid);
    /*     file_op_context_destroy(ctx); */
    return 1;
}
#endif
/* }}} */

/* --------------------------------------------------------------------------------------------- */

/**
 * On Solaris, ENOTSUP != EOPNOTSUPP. Some FS also return ENOSYS or EINVAL as "not implemented".
 * On some Linux kernels (tested on 4.9, 5.4) there is ENOTTY on tmpfs.
 */
static inline gboolean
attrs_ignore_error (const int e)
{
    return (e == ENOTSUP || e == EOPNOTSUPP || e == ENOSYS || e == EINVAL || e == ENOTTY
            || e == ELOOP || e == ENXIO);
}

/* --------------------------------------------------------------------------------------------- */
/*** public functions ****************************************************************************/
/* --------------------------------------------------------------------------------------------- */

/* Is file symlink to directory or not.
 *
 * @param path file or directory
 * @param st result of mc_lstat(vpath). If NULL, mc_lstat(vpath) is performed here
 * @param stale_link TRUE if file is stale link to directory
 *
 * @return TRUE if file symlink to directory, ELSE otherwise.
 */
gboolean
file_is_symlink_to_dir (const vfs_path_t *vpath, mc_stat_t *st, gboolean *stale_link)
{
    mc_stat_t st2;
    gboolean stale = FALSE;
    gboolean res = FALSE;

    if (st == NULL)
    {
        st = &st2;

        if (mc_lstat (vpath, st) != 0)
            goto ret;
    }

    if (S_ISLNK (st->st_mode))
    {
        mc_stat_t st3;

        stale = (mc_stat (vpath, &st3) != 0);

        if (!stale)
            res = (S_ISDIR (st3.st_mode) != 0);
    }

  ret:
    if (stale_link != NULL)
        *stale_link = stale;

    return res;
}

/* --------------------------------------------------------------------------------------------- */

FileProgressStatus
copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_path)
{
    uid_t src_uid = (uid_t) (-1);
    gid_t src_gid = (gid_t) (-1);

    int src_desc, dest_desc = -1;
    mode_t src_mode = 0;        /* The mode of the source file */
    mc_stat_t src_stat, dst_stat;
    mc_timesbuf_t times;
    unsigned long attrs = 0;
    gboolean attrs_ok = ctx->preserve;
    gboolean dst_exists = FALSE, appending = FALSE;
    mc_off_t file_size = -1;
    FileProgressStatus return_status, temp_status;
    dest_status_t dst_status = DEST_NONE;
    int open_flags;
    vfs_path_t *src_vpath = NULL, *dst_vpath = NULL;
    char *buf = NULL;

    /* Keep the non-default value applied in chain of calls:
       move_file_file() -> file_progress_real_query_replace()
       move_file_file() -> copy_file_file() */
    if (ctx->do_reget < 0)
        ctx->do_reget = 0;

    return_status = FILE_RETRY;

    dst_vpath = vfs_path_from_str (dst_path);
    src_vpath = vfs_path_from_str (src_path);

    file_progress_show_source (ctx, src_vpath);
    file_progress_show_target (ctx, dst_vpath);

    if (file_progress_check_buttons (ctx) == FILE_ABORT)
    {
        return_status = FILE_ABORT;
        goto ret_fast;
    }

    mc_refresh ();

    while (mc_stat (dst_vpath, &dst_stat) == 0)
    {
        if (S_ISDIR (dst_stat.st_mode))
        {
            if (ctx->ignore_all)
                return_status = FILE_IGNORE_ALL;
            else
            {
                return_status =
                    file_error (ctx, TRUE, _("Cannot overwrite directory \"%s\"\n%s"), dst_path);
                if (return_status == FILE_IGNORE_ALL)
                    ctx->ignore_all = TRUE;
                if (return_status == FILE_RETRY)
                    continue;
            }
            goto ret_fast;
        }

        dst_exists = TRUE;
        break;
    }

    while ((*ctx->stat_func) (src_vpath, &src_stat) != 0)
    {
        if (ctx->ignore_all)
            return_status = FILE_IGNORE_ALL;
        else
        {
            return_status =
                file_error (ctx, TRUE, _("Cannot stat source file \"%s\"\n%s"), src_path);
            if (return_status == FILE_IGNORE_ALL)
                ctx->ignore_all = TRUE;
        }

        if (return_status != FILE_RETRY)
        {
            /* unknown size */
            progress_update_one (FALSE, ctx, 0);
            goto ret_fast;
        }
    }

    /* After ctx->stat_func() */
    src_mode = src_stat.st_mode;
    src_uid = src_stat.st_uid;
    src_gid = src_stat.st_gid;
    file_size = src_stat.st_size;

    while (attrs_ok && mc_fgetflags (src_vpath, &attrs) != 0)
    {
        attrs_ok = FALSE;

        /* don't show an error message if attributes aren't supported in this FS */
        if (attrs_ignore_error (errno))
            return_status = FILE_CONT;
        else if (ctx->ignore_all)
            return_status = FILE_IGNORE_ALL;
        else
        {
            return_status =
                file_error (ctx, TRUE, _("Cannot get ext2 attributes of source file \"%s\"\n%s"),
                            src_path);
            if (return_status == FILE_IGNORE_ALL)
                ctx->ignore_all = TRUE;
            if (return_status == FILE_ABORT)
                goto ret_fast;
        }

        if (return_status != FILE_RETRY)
            break;

        /* yet another attempt */
        attrs_ok = TRUE;
    }

    if (dst_exists)
    {
        /* Destination already exists */
        if (check_same_file (ctx, src_path, &src_stat, dst_path, &dst_stat, &return_status))
            goto ret_fast;

        /* Should we replace destination? */
        if (ctx->ask_overwrite)
        {
            ctx->do_reget = 0;
            return_status = query_replace (ctx, src_path, &src_stat, dst_path, &dst_stat);
            if (return_status != FILE_CONT)
                goto ret_fast;
        }
    }

    vfs_get_timesbuf_from_stat (&src_stat, &times);

    if (!ctx->do_append)
    {
        /* Check the hardlinks */
        if (!ctx->follow_links)
        {
            switch (check_hardlinks (ctx, src_vpath, &src_stat, dst_vpath, &ctx->ignore_all))
            {
            case HARDLINK_OK:
                /* We have made a hardlink - no more processing is necessary */
                return_status = FILE_CONT;
                goto ret_fast;

            case HARDLINK_ABORT:
                return_status = FILE_ABORT;
                goto ret_fast;

            default:
                break;
            }
        }

        if (S_ISLNK (src_stat.st_mode))
        {
            return_status = make_symlink (ctx, src_vpath, dst_vpath);
            if (return_status == FILE_CONT && ctx->preserve)
            {
                mc_utime (dst_vpath, &times);

                while (attrs_ok && mc_fsetflags (dst_vpath, attrs) != 0 && !ctx->ignore_all)
                {
                    attrs_ok = FALSE;

                    /* don't show an error message if attributes aren't supported in this FS */
                    if (attrs_ignore_error (errno))
                        return_status = FILE_CONT;
                    else if (return_status == FILE_IGNORE_ALL)
                        ctx->ignore_all = TRUE;
                    else
                        return_status =
                            file_error (ctx, TRUE,
                                        _("Cannot set ext2 attributes of target file \"%s\"\n%s"),
                                        dst_path);

                    if (return_status != FILE_RETRY)
                        break;

                    /* yet another attempt */
                    attrs_ok = TRUE;
                }
            }
            goto ret_fast;
        }

        if (S_ISCHR (src_stat.st_mode) || S_ISBLK (src_stat.st_mode) || S_ISFIFO (src_stat.st_mode)
            || S_ISNAM (src_stat.st_mode) || S_ISSOCK (src_stat.st_mode))
        {
            dev_t rdev = 0;

#ifdef HAVE_STRUCT_STAT_ST_RDEV
            rdev = src_stat.st_rdev;
#endif

            while (mc_mknod (dst_vpath, src_stat.st_mode & ctx->umask_kill, rdev) < 0
                   && !ctx->ignore_all)
            {
                return_status =
                    file_error (ctx, TRUE, _("Cannot create special file \"%s\"\n%s"), dst_path);
                if (return_status == FILE_RETRY)
                    continue;
                if (return_status == FILE_IGNORE_ALL)
                    ctx->ignore_all = TRUE;
                goto ret_fast;
            }
            /* Success */

            while (ctx->preserve_uidgid
                   && mc_chown (dst_vpath, src_stat.st_uid, src_stat.st_gid) != 0
                   && !ctx->ignore_all)
            {
                temp_status =
                    file_error (ctx, TRUE, _("Cannot chown target file \"%s\"\n%s"), dst_path);
                if (temp_status == FILE_IGNORE)
                    break;
                if (temp_status == FILE_IGNORE_ALL)
                    ctx->ignore_all = TRUE;
                if (temp_status != FILE_RETRY)
                {
                    return_status = temp_status;
                    goto ret_fast;
                }
            }

            while (ctx->preserve && mc_chmod (dst_vpath, src_stat.st_mode & ctx->umask_kill) != 0
                   && !ctx->ignore_all)
            {
                temp_status =
                    file_error (ctx, TRUE, _("Cannot chmod target file \"%s\"\n%s"), dst_path);
                if (temp_status == FILE_IGNORE)
                    break;
                if (temp_status == FILE_IGNORE_ALL)
                    ctx->ignore_all = TRUE;
                if (temp_status != FILE_RETRY)
                {
                    return_status = temp_status;
                    goto ret_fast;
                }
            }

            while (attrs_ok && mc_fsetflags (dst_vpath, attrs) != 0 && !ctx->ignore_all)
            {
                attrs_ok = FALSE;

                /* don't show an error message if attributes aren't supported in this FS */
                if (attrs_ignore_error (errno))
                    break;

                temp_status =
                    file_error (ctx, TRUE, _("Cannot set ext2 attributes of target file \"%s\"\n%s"),
                                dst_path);
                if (temp_status == FILE_IGNORE)
                    break;
                if (temp_status == FILE_IGNORE_ALL)
                    ctx->ignore_all = TRUE;
                if (temp_status != FILE_RETRY)
                {
                    return_status = temp_status;
                    goto ret_fast;
                }

                /* yet another attempt */
                attrs_ok = TRUE;
            }

            return_status = FILE_CONT;
            mc_utime (dst_vpath, &times);
            goto ret_fast;
        }
    }

    ctx->transfer_start = g_get_monotonic_time ();

    while ((src_desc = mc_open (src_vpath, O_RDONLY | O_LINEAR)) < 0)
    {
        if (ctx->ignore_all)
            return_status = FILE_IGNORE_ALL;
        else
        {
            return_status =
                file_error (ctx, TRUE, _("Cannot open source file \"%s\"\n%s"), src_path);
            if (return_status == FILE_RETRY)
                continue;
            if (return_status == FILE_IGNORE_ALL)
                ctx->ignore_all = TRUE;
            ctx->do_append = FALSE;
        }
        goto ret;
    }

    if (ctx->do_reget != 0 && mc_lseek (src_desc, ctx->do_reget, SEEK_SET) != ctx->do_reget)
    {
        message (D_ERROR, _("Warning"), _("Reget failed, about to overwrite file"));
        ctx->do_reget = 0;
        ctx->do_append = FALSE;
    }

    while (mc_fstat (src_desc, &src_stat) != 0)
    {
        if (ctx->ignore_all)
            return_status = FILE_IGNORE_ALL;
        else
        {
            return_status =
                file_error (ctx, TRUE, _("Cannot fstat source file \"%s\"\n%s"), src_path);
            if (return_status == FILE_RETRY)
                continue;
            if (return_status == FILE_IGNORE_ALL)
                ctx->ignore_all = TRUE;
            ctx->do_append = FALSE;
        }
        goto ret;
    }

    /* After mc_fstat() */
    src_mode = src_stat.st_mode;
    src_uid = src_stat.st_uid;
    src_gid = src_stat.st_gid;
    file_size = src_stat.st_size;

    open_flags = O_WRONLY;
    if (!dst_exists)
        open_flags |= O_CREAT | O_EXCL;
    else if (ctx->do_append)
        open_flags |= O_APPEND;
    else
        open_flags |= O_CREAT | O_TRUNC;

    while ((dest_desc = mc_open (dst_vpath, open_flags, src_mode)) < 0)
    {
        if (errno != EEXIST)
        {
            if (ctx->ignore_all)
                return_status = FILE_IGNORE_ALL;
            else
            {
                return_status =
                    file_error (ctx, TRUE, _("Cannot create target file \"%s\"\n%s"), dst_path);
                if (return_status == FILE_RETRY)
                    continue;
                if (return_status == FILE_IGNORE_ALL)
                    ctx->ignore_all = TRUE;
                ctx->do_append = FALSE;
            }
        }
        goto ret;
    }

    /* file opened, but not fully copied */
    dst_status = DEST_SHORT_QUERY;

    appending = ctx->do_append;
    ctx->do_append = FALSE;

    /* Try clone the file first. */
    if (vfs_clone_file (dest_desc, src_desc) == 0)
    {
        dst_status = DEST_FULL;
        return_status = FILE_CONT;
        goto ret;
    }

    /* Find out the optimal buffer size.  */
    while (mc_fstat (dest_desc, &dst_stat) != 0)
    {
        if (ctx->ignore_all)
            return_status = FILE_IGNORE_ALL;
        else
        {
            return_status =
                file_error (ctx, TRUE, _("Cannot fstat target file \"%s\"\n%s"), dst_path);
            if (return_status == FILE_RETRY)
                continue;
            if (return_status == FILE_IGNORE_ALL)
                ctx->ignore_all = TRUE;
        }
        goto ret;
    }

    /* try preallocate space; if fail, try copy anyway */
    while (mc_global.vfs.preallocate_space &&
           vfs_preallocate (dest_desc, file_size, appending ? dst_stat.st_size : 0) != 0)
    {
        if (ctx->ignore_all)
        {
            /* cannot allocate, start the file copying anyway */
            return_status = FILE_CONT;
            break;
        }

        return_status =
            file_error (ctx, TRUE, _("Cannot preallocate space for target file \"%s\"\n%s"),
                        dst_path);

        if (return_status == FILE_IGNORE_ALL)
            ctx->ignore_all = TRUE;

        if (ctx->ignore_all || return_status == FILE_IGNORE)
        {
            /* skip the space allocation error, start file copying */
            return_status = FILE_CONT;
            break;
        }

        if (return_status == FILE_ABORT)
        {
            mc_close (dest_desc);
            dest_desc = -1;
            mc_unlink (dst_vpath);
            dst_status = DEST_NONE;
            goto ret;
        }

        /* return_status == FILE_RETRY -- try allocate space again */
    }

    ctx->eta_secs = 0.0;
    ctx->bps = 0;

    if (verbose)
    {
        if (ctx->total_bps == 0 || (file_size / ctx->total_bps) > FILEOP_UPDATE_INTERVAL)
            file_progress_show (ctx, 0, file_size, "", TRUE);
        else
            file_progress_show (ctx, 1, 1, "", TRUE);
    }

    return_status = file_progress_check_buttons (ctx);
    mc_refresh ();

    if (return_status == FILE_CONT)
    {
        mc_off_t file_part = 0;
        gint64 tv_last_update = ctx->transfer_start;
        gint64 tv_last_input = 0;
        gboolean is_first_time = TRUE;

        const size_t bufsize = io_blksize (dst_stat);
        buf = g_malloc (bufsize);

        while (TRUE)
        {
            ssize_t n_read = -1;

            /* src_read */
            if (mc_ctl (src_desc, VFS_CTL_IS_NOTREADY, 0) == 0)
                while ((n_read = mc_read (src_desc, buf, bufsize)) < 0 && !ctx->ignore_all)
                {
                    return_status =
                        file_error (ctx, TRUE, _("Cannot read source file \"%s\"\n%s"), src_path);
                    if (return_status == FILE_RETRY)
                        continue;
                    if (return_status == FILE_IGNORE_ALL)
                        ctx->ignore_all = TRUE;
                    goto ret;
                }

            if (n_read == 0)
                break;

            const gint64 tv_current = g_get_monotonic_time ();

            if (n_read > 0)
            {
                ssize_t n_written;
                char *t = buf;

                file_part += n_read;

                tv_last_input = tv_current;

                /* dst_write */
                while ((n_written = mc_write (dest_desc, t, (size_t) n_read)) < n_read)
                {
                    gboolean write_errno_nospace;

                    if (n_written > 0)
                    {
                        n_read -= n_written;
                        t += n_written;
                        continue;
                    }

                    write_errno_nospace = (n_written < 0 && errno == ENOSPC);

                    if (ctx->ignore_all)
                        return_status = FILE_IGNORE_ALL;
                    else
                        return_status =
                            file_error (ctx, TRUE, _("Cannot write target file \"%s\"\n%s"),
                                        dst_path);

                    if (return_status == FILE_IGNORE)
                    {
                        if (write_errno_nospace)
                            goto ret;
                        break;
                    }
                    if (return_status == FILE_IGNORE_ALL)
                    {
                        ctx->ignore_all = TRUE;
                        if (write_errno_nospace)
                            goto ret;
                    }
                    if (return_status != FILE_RETRY)
                        goto ret;
                }
            }

            ctx->progress_bytes = file_part + ctx->do_reget;

            const gint64 usecs = tv_current - tv_last_update;

            if (is_first_time || usecs > FILEOP_UPDATE_INTERVAL_US)
            {
                calc_copy_file_progress (ctx, tv_current, file_part, file_size - ctx->do_reget);
                tv_last_update = tv_current;
            }

            is_first_time = FALSE;

            if (verbose)
            {
                const gint64 total_usecs = tv_current - ctx->total_transfer_start;
                const gboolean force_update = total_usecs > FILEOP_UPDATE_INTERVAL_US;

                const gint64 update_usecs = tv_current - tv_last_input;
                const char *stalled_msg =
                    update_usecs > FILEOP_STALLING_INTERVAL_US ? _("(stalled)") : "";

                file_progress_show (ctx, ctx->progress_bytes, file_size, stalled_msg, force_update);
                if (ctx->dialog_type == FILEGUI_DIALOG_MULTI_ITEM)
                {
                    file_progress_show_count (ctx);
                    file_progress_show_total (ctx, ctx->total_progress_bytes + ctx->progress_bytes,
                                              tv_current, force_update);
                }

                mc_refresh ();
            }

            return_status = file_progress_check_buttons (ctx);
            if (return_status != FILE_CONT)
            {
                int query_res;

                const gint64 t1 = g_get_monotonic_time ();
                query_res =
                    query_dialog (Q_ ("DialogTitle|Copy"),
                                  _("Incomplete file was retrieved"), D_ERROR, 3,
                                  _("&Delete"), _("&Keep"), _("&Continue copy"));
                const gint64 t2 = g_get_monotonic_time ();
                ctx->pauses += t2 - t1;

                /* update info forced */
                calc_copy_file_progress (ctx, t2, file_part, file_size - ctx->do_reget);

                switch (query_res)
                {
                case 0:
                    /* delete */
                    dst_status = DEST_SHORT_DELETE;
                    goto ret;

                case 1:
                    /* keep */
                    dst_status = DEST_SHORT_KEEP;
                    goto ret;

                default:
                    /* continue copy */
                    break;
                }
            }
        }

        /* copy successful */
        dst_status = DEST_FULL;
    }

  ret:
    g_free (buf);

    rotate_dash (FALSE);
    while (src_desc != -1 && mc_close (src_desc) < 0 && !ctx->ignore_all)
    {
        temp_status = file_error (ctx, TRUE, _("Cannot close source file \"%s\"\n%s"), src_path);
        if (temp_status == FILE_RETRY)
            continue;
        if (temp_status == FILE_ABORT)
            return_status = temp_status;
        if (temp_status == FILE_IGNORE_ALL)
            ctx->ignore_all = TRUE;
        break;
    }

    while (dest_desc != -1 && mc_close (dest_desc) < 0 && !ctx->ignore_all)
    {
        temp_status = file_error (ctx, TRUE, _("Cannot close target file \"%s\"\n%s"), dst_path);
        if (temp_status == FILE_RETRY)
            continue;
        if (temp_status == FILE_IGNORE_ALL)
            ctx->ignore_all = TRUE;
        return_status = temp_status;
        break;
    }

    if (dst_status == DEST_SHORT_QUERY)
    {
        /* Query to remove short file */
        if (query_dialog (Q_ ("DialogTitle|Copy"), _("Incomplete file was retrieved"),
                          D_ERROR, 2, _("&Delete"), _("&Keep")) == 0)
            dst_status = DEST_SHORT_DELETE;
        else
            dst_status = DEST_SHORT_KEEP;
    }

    if (dst_status == DEST_SHORT_DELETE)
        mc_unlink (dst_vpath);
    else if (dst_status == DEST_FULL && !appending)
    {
        /* Copy has succeeded */

        while (ctx->preserve_uidgid && mc_chown (dst_vpath, src_uid, src_gid) != 0
               && !ctx->ignore_all)
        {
            temp_status = file_error (ctx, TRUE, _("Cannot chown target file \"%s\"\n%s"), dst_path);
            if (temp_status == FILE_ABORT)
            {
                return_status = FILE_ABORT;
                goto ret_fast;
            }
            if (temp_status == FILE_RETRY)
                continue;
            if (temp_status == FILE_IGNORE_ALL)
            {
                ctx->ignore_all = TRUE;
                return_status = FILE_CONT;
            }
            if (temp_status == FILE_IGNORE)
                return_status = FILE_CONT;
            break;
        }

        while (ctx->preserve && mc_chmod (dst_vpath, (src_mode & ctx->umask_kill)) != 0
               && !ctx->ignore_all)
        {
            temp_status = file_error (ctx, TRUE, _("Cannot chmod target file \"%s\"\n%s"), dst_path);
            if (temp_status == FILE_ABORT)
            {
                return_status = FILE_ABORT;
                goto ret_fast;
            }
            if (temp_status == FILE_RETRY)
                continue;
            if (temp_status == FILE_IGNORE_ALL)
            {
                ctx->ignore_all = TRUE;
                return_status = FILE_CONT;
            }
            if (temp_status == FILE_IGNORE)
                return_status = FILE_CONT;
            break;
        }

        if (!ctx->preserve && !dst_exists)
        {
            src_mode = umask (-1);
            umask (src_mode);
            src_mode = 0100666 & ~src_mode;
            mc_chmod (dst_vpath, (src_mode & ctx->umask_kill));
        }
    }

    if (dst_status == DEST_FULL || dst_status == DEST_SHORT_KEEP)
    {
        /* Always sync timestamps */
        mc_utime (dst_vpath, &times);

        while (attrs_ok && mc_fsetflags (dst_vpath, attrs) != 0 && !ctx->ignore_all)
        {
            attrs_ok = FALSE;

            /* don't show an error message if attributes aren't supported in this FS */
            if (attrs_ignore_error (errno))
            {
                return_status = FILE_CONT;
                break;
            }

            temp_status =
                file_error (ctx, TRUE, _("Cannot set ext2 attributes for target file \"%s\"\n%s"),
                            dst_path);
            if (temp_status == FILE_ABORT)
                return_status = FILE_ABORT;
            if (temp_status == FILE_RETRY)
            {
                attrs_ok = TRUE;
                continue;
            }
            if (temp_status == FILE_IGNORE_ALL)
            {
                ctx->ignore_all = TRUE;
                return_status = FILE_CONT;
            }
            if (temp_status == FILE_IGNORE)
                return_status = FILE_CONT;
            break;
        }
    }

    progress_update_one (return_status == FILE_CONT, ctx, file_size);
    if (return_status == FILE_CONT)
        return_status = file_progress_check_buttons (ctx);

  ret_fast:
    vfs_path_free (src_vpath, TRUE);
    vfs_path_free (dst_vpath, TRUE);
    return return_status;
}

/* --------------------------------------------------------------------------------------------- */
/**
 * I think these copy_*_* functions should have a return type.
 * anyway, this function *must* have two directories as arguments.
 */
/* FIXME: This function needs to check the return values of the
   function calls */

FileProgressStatus
copy_dir_dir (file_op_context_t *ctx, const char *s, const char *d, gboolean toplevel,
              gboolean move_over, gboolean do_delete, GSList *parent_dirs)
{
    struct vfs_dirent *next;
    mc_stat_t dst_stat, src_stat;
    unsigned long attrs = 0;
    gboolean attrs_ok = ctx->preserve;
    DIR *reading;
    FileProgressStatus return_status = FILE_CONT;
    link_t *lp;
    vfs_path_t *src_vpath, *dst_vpath;
    gboolean do_mkdir = TRUE;

    src_vpath = vfs_path_from_str (s);
    dst_vpath = vfs_path_from_str (d);

    /* First get the mode of the source dir */

  retry_src_stat:
    while ((*ctx->stat_func) (src_vpath, &src_stat) != 0)
    {
        if (ctx->ignore_all)
            return_status = FILE_IGNORE_ALL;
        else
        {
            return_status = file_error (ctx, TRUE, _("Cannot stat source directory \"%s\"\n%s"), s);
            if (return_status == FILE_IGNORE_ALL)
                ctx->ignore_all = TRUE;
        }

        if (return_status != FILE_RETRY)
            goto ret_fast;
    }

    while (attrs_ok && mc_fgetflags (src_vpath, &attrs) != 0)
    {
        attrs_ok = FALSE;

        /* don't show an error message if attributes aren't supported in this FS */
        if (attrs_ignore_error (errno))
            return_status = FILE_CONT;
        else if (ctx->ignore_all)
            return_status = FILE_IGNORE_ALL;
        else
        {
            return_status =
                file_error (ctx, TRUE,
                            _("Cannot get ext2 attributes of source directory \"%s\"\n%s"), s);
            if (return_status == FILE_IGNORE_ALL)
                ctx->ignore_all = TRUE;
            if (return_status == FILE_ABORT)
                goto ret_fast;
        }

        if (return_status != FILE_RETRY)
            break;

        /* yet another attempt */
        attrs_ok = TRUE;
    }

    if (is_in_linklist (dest_dirs, src_vpath, &src_stat) != NULL)
    {
        /* Don't copy a directory we created before (we don't want to copy
           infinitely if a directory is copied into itself) */
        /* FIXME: should there be an error message and FILE_SKIP? - Norbert */
        return_status = FILE_CONT;
        goto ret_fast;
    }

    /* Hmm, hardlink to directory??? - Norbert */
    /* FIXME: In this step we should do something in case the destination already exist */
    /* Check the hardlinks */
    if (ctx->preserve)
    {
        switch (check_hardlinks (ctx, src_vpath, &src_stat, dst_vpath, &ctx->ignore_all))
        {
        case HARDLINK_OK:
            /* We have made a hardlink - no more processing is necessary */
            goto ret_fast;

        case HARDLINK_ABORT:
            return_status = FILE_ABORT;
            goto ret_fast;

        default:
            break;
        }
    }

    if (!S_ISDIR (src_stat.st_mode))
    {
        if (ctx->ignore_all)
            return_status = FILE_IGNORE_ALL;
        else
        {
            return_status = file_error (ctx, TRUE, _("Source \"%s\" is not a directory\n%s"), s);
            if (return_status == FILE_RETRY)
                goto retry_src_stat;
            if (return_status == FILE_IGNORE_ALL)
                ctx->ignore_all = TRUE;
        }
        goto ret_fast;
    }

    if (is_in_linklist (parent_dirs, src_vpath, &src_stat) != NULL)
    {
        /* we found a cyclic symbolic link */
        message (D_ERROR, MSG_ERROR, _("Cannot copy cyclic symbolic link\n\"%s\""), s);
        return_status = FILE_SKIP;
        goto ret_fast;
    }

    lp = g_new0 (link_t, 1);
    lp->vfs = vfs_path_get_last_path_vfs (src_vpath);
    lp->ino = src_stat.st_ino;
    lp->dev = src_stat.st_dev;
    parent_dirs = g_slist_prepend (parent_dirs, lp);

  retry_dst_stat:
    /* Now, check if the dest dir exists, if not, create it. */
    if (mc_stat (dst_vpath, &dst_stat) != 0)
    {
        /* Here the dir doesn't exist : make it ! */
        if (move_over && mc_rename (src_vpath, dst_vpath) == 0)
        {
            return_status = FILE_CONT;
            goto ret;
        }
    }
    else
    {
        /*
         * If the destination directory exists, we want to copy the whole
         * directory, but we only want this to happen once.
         *
         * Escape sequences added to the * to compiler warnings.
         * so, say /bla exists, if we copy /tmp/\* to /bla, we get /bla/tmp/\*
         * or ( /bla doesn't exist )       /tmp/\* to /bla     ->  /bla/\*
         */
        if (!S_ISDIR (dst_stat.st_mode))
        {
            if (ctx->ignore_all)
                return_status = FILE_IGNORE_ALL;
            else
            {
                return_status =
                    file_error (ctx, TRUE, _("Destination \"%s\" must be a directory\n%s"), d);
                if (return_status == FILE_IGNORE_ALL)
                    ctx->ignore_all = TRUE;
                if (return_status == FILE_RETRY)
                    goto retry_dst_stat;
            }
            goto ret;
        }
        /* Dive into subdir if exists */
        if (toplevel && ctx->dive_into_subdirs)
        {
            vfs_path_t *tmp;

            tmp = dst_vpath;
            dst_vpath = vfs_path_append_new (dst_vpath, x_basename (s), (char *) NULL);
            vfs_path_free (tmp, TRUE);

        }
        else
            do_mkdir = FALSE;
    }

    d = vfs_path_as_str (dst_vpath);

    if (do_mkdir)
    {
        while (my_mkdir (dst_vpath, (src_stat.st_mode & ctx->umask_kill) | S_IRWXU) != 0)
        {
            if (ctx->ignore_all)
                return_status = FILE_IGNORE_ALL;
            else
            {
                return_status =
                    file_error (ctx, TRUE, _("Cannot create target directory \"%s\"\n%s"), d);
                if (return_status == FILE_IGNORE_ALL)
                    ctx->ignore_all = TRUE;
            }
            if (return_status != FILE_RETRY)
                goto ret;
        }

        lp = g_new0 (link_t, 1);
        mc_stat (dst_vpath, &dst_stat);
        lp->vfs = vfs_path_get_last_path_vfs (dst_vpath);
        lp->ino = dst_stat.st_ino;
        lp->dev = dst_stat.st_dev;
        dest_dirs = g_slist_prepend (dest_dirs, lp);
    }

    if (ctx->preserve_uidgid)
    {
        while (mc_chown (dst_vpath, src_stat.st_uid, src_stat.st_gid) != 0)
        {
            if (ctx->ignore_all)
                return_status = FILE_IGNORE_ALL;
            else
            {
                return_status =
                    file_error (ctx, TRUE, _("Cannot chown target directory \"%s\"\n%s"), d);
                if (return_status == FILE_IGNORE_ALL)
                    ctx->ignore_all = TRUE;
            }
            if (return_status != FILE_RETRY)
                goto ret;
        }
    }

    /* open the source dir for reading */
    reading = mc_opendir (src_vpath);
    if (reading == NULL)
        goto ret;

    while ((next = mc_readdir (reading)) && return_status != FILE_ABORT)
    {
        char *path;
        vfs_path_t *tmp_vpath;

        /*
         * Now, we don't want '.' and '..' to be created / copied at any time
         */
        if (DIR_IS_DOT (next->d_name) || DIR_IS_DOTDOT (next->d_name))
            continue;

        /* get the filename and add it to the src directory */
        path = mc_build_filename (s, next->d_name, (char *) NULL);
        tmp_vpath = vfs_path_from_str (path);

        (*ctx->stat_func) (tmp_vpath, &dst_stat);
        if (S_ISDIR (dst_stat.st_mode))
        {
            char *mdpath;

            mdpath = mc_build_filename (d, next->d_name, (char *) NULL);
            /*
             * From here, we just intend to recursively copy subdirs, not
             * the double functionality of copying different when the target
             * dir already exists. So, we give the recursive call the flag 0
             * meaning no toplevel.
             */
            return_status = copy_dir_dir (ctx, path, mdpath, FALSE, FALSE, do_delete, parent_dirs);
            g_free (mdpath);
        }
        else
        {
            char *dest_file;

            dest_file = mc_build_filename (d, x_basename (path), (char *) NULL);
            return_status = copy_file_file (ctx, path, dest_file);
            g_free (dest_file);
        }

        g_free (path);

        if (do_delete && return_status == FILE_CONT)
        {
            if (ctx->erase_at_end)
            {
                if (erase_list == NULL)
                    erase_list = g_queue_new ();

                lp = g_new0 (link_t, 1);
                lp->src_vpath = tmp_vpath;
                lp->st_mode = dst_stat.st_mode;
                g_queue_push_tail (erase_list, lp);
                tmp_vpath = NULL;
            }
            else if (S_ISDIR (dst_stat.st_mode))
                return_status = erase_dir_iff_empty (ctx, tmp_vpath);
            else
                return_status = erase_file (ctx, tmp_vpath);
        }
        vfs_path_free (tmp_vpath, TRUE);
    }
    mc_closedir (reading);

    if (ctx->preserve)
    {
        mc_timesbuf_t times;

        mc_chmod (dst_vpath, src_stat.st_mode & ctx->umask_kill);

        if (attrs_ok)
            mc_fsetflags (dst_vpath, attrs);

        vfs_get_timesbuf_from_stat (&src_stat, &times);
        mc_utime (dst_vpath, &times);
    }
    else
    {
        src_stat.st_mode = umask (-1);
        umask (src_stat.st_mode);
        src_stat.st_mode = 0100777 & ~src_stat.st_mode;
        mc_chmod (dst_vpath, src_stat.st_mode & ctx->umask_kill);
    }

  ret:
    free_link (parent_dirs->data);
    g_slist_free_1 (parent_dirs);
  ret_fast:
    vfs_path_free (src_vpath, TRUE);
    vfs_path_free (dst_vpath, TRUE);
    return return_status;
}

/* }}} */

/* --------------------------------------------------------------------------------------------- */
/* {{{ Move routines */

FileProgressStatus
move_dir_dir (file_op_context_t *ctx, const char *s, const char *d)
{
    return do_move_dir_dir (NULL, ctx, s, d);
}

/* }}} */

/* --------------------------------------------------------------------------------------------- */
/* {{{ Erase routines */

FileProgressStatus
erase_dir (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 ();

    /* The old way to detect a non empty directory was:
       error = my_rmdir (s);
       if (error && (errno == ENOTEMPTY || errno == EEXIST))){
       For the linux user space nfs server (nfs-server-2.2beta29-2)
       we would have to check also for EIO. I hope the new way is
       fool proof. (Norbert)
     */
    const int res = check_dir_is_empty (ctx, vpath, &error);

    if (res == -1)
        return error;

    if (res == 0)
    {
        /* not empty */
        error = query_recursive (ctx, vfs_path_as_str (vpath));
        if (error == FILE_CONT)
            error = recursive_erase (ctx, vpath);
        return error;
    }

    return try_erase_dir (ctx, vpath);
}

/* }}} */

/* --------------------------------------------------------------------------------------------- */
/* {{{ Panel operate routines */

void
dirsize_status_init_cb (status_msg_t *sm)
{
    dirsize_status_msg_t *dsm = (dirsize_status_msg_t *) sm;
    WGroup *gd = GROUP (sm->dlg);
    Widget *wd = WIDGET (sm->dlg);
    WRect r = wd->rect;

    const char *b1_name = N_("&Abort");
    const char *b2_name = N_("&Skip");
    int b_width, ui_width;

#ifdef ENABLE_NLS
    b1_name = _(b1_name);
    b2_name = _(b2_name);
#endif

    b_width = str_term_width1 (b1_name) + 4;
    if (dsm->allow_skip)
        b_width += str_term_width1 (b2_name) + 4 + 1;

    ui_width = MAX (COLS / 2, b_width + 6);
    dsm->dirname = label_new (2, 3, NULL);
    group_add_widget (gd, dsm->dirname);
    dsm->count_size = label_new (3, 3, NULL);
    group_add_widget (gd, dsm->count_size);
    group_add_widget (gd, hline_new (4, -1, -1));

    dsm->abort_button = WIDGET (button_new (5, 3, FILE_ABORT, NORMAL_BUTTON, b1_name, NULL));
    group_add_widget (gd, dsm->abort_button);
    if (dsm->allow_skip)
    {
        dsm->skip_button = WIDGET (button_new (5, 3, FILE_SKIP, NORMAL_BUTTON, b2_name, NULL));
        group_add_widget (gd, dsm->skip_button);
        widget_select (dsm->skip_button);
    }

    r.lines = 8;
    r.cols = ui_width;
    widget_set_size_rect (wd, &r);
    dirsize_status_locate_buttons (dsm);
}

/* --------------------------------------------------------------------------------------------- */

int
dirsize_status_update_cb (status_msg_t *sm)
{
    dirsize_status_msg_t *dsm = (dirsize_status_msg_t *) sm;
    Widget *wd = WIDGET (sm->dlg);
    WRect r = wd->rect;

    /* update second (longer label) */
    label_set_textv (dsm->count_size, _("Directories: %zu, total size: %s"),
                     dsm->dir_count, size_trunc_sep (dsm->total_size, panels_options.kilobyte_si));

    /* enlarge dialog if required */
    if (WIDGET (dsm->count_size)->rect.cols + 6 > r.cols)
    {
        r.cols = WIDGET (dsm->count_size)->rect.cols + 6;
        widget_set_size_rect (wd, &r);
        dirsize_status_locate_buttons (dsm);
        widget_draw (wd);
        /* TODO: ret rid of double redraw */
    }

    /* adjust first label */
    label_set_text (dsm->dirname,
                    str_trunc (vfs_path_as_str (dsm->dirname_vpath), wd->rect.cols - 6));

    switch (status_msg_common_update (sm))
    {
    case B_CANCEL:
    case FILE_ABORT:
        return FILE_ABORT;
    case FILE_SKIP:
        return FILE_SKIP;
    default:
        return FILE_CONT;
    }
}

/* --------------------------------------------------------------------------------------------- */

void
dirsize_status_deinit_cb (status_msg_t *sm)
{
    (void) sm;

    /* schedule to update passive panel */
    if (get_other_type () == view_listing)
        other_panel->dirty = TRUE;
}

/* --------------------------------------------------------------------------------------------- */
/**
 * compute_dir_size:
 *
 * Computes the number of bytes used by the files in a directory
 */

FileProgressStatus
compute_dir_size (const vfs_path_t *dirname_vpath, dirsize_status_msg_t *sm,
                  size_t *ret_dir_count, size_t *ret_marked_count, uintmax_t *ret_total,
                  gboolean follow_symlinks)
{
    return do_compute_dir_size (dirname_vpath, sm, ret_dir_count, ret_marked_count, ret_total,
                                follow_symlinks ? mc_stat : mc_lstat);
}

/* --------------------------------------------------------------------------------------------- */
/**
 * panel_operate:
 *
 * Performs one of the operations on the current on the source_panel
 * (copy, delete, move).
 *
 * Returns TRUE if did change the directory
 * structure, Returns FALSE if user aborted
 *
 * force_single forces operation on the current entry and affects
 * default destination.  Current filename is used as default.
 */

gboolean
panel_operate (void *source_panel, FileOperation operation, gboolean force_single)
{
    WPanel *panel = PANEL (source_panel);
    const gboolean single_entry = force_single || (panel->marked <= 1)
        || (get_current_type () == view_tree);

    const char *source = NULL;
    char *dest = NULL;
    vfs_path_t *dest_vpath = NULL;
    vfs_path_t *save_cwd = NULL, *save_dest = NULL;
    mc_stat_t src_stat;
    gboolean ret_val = TRUE;
    int i;
    FileProgressStatus value;
    file_op_context_t *ctx;
    filegui_dialog_type_t dialog_type = FILEGUI_DIALOG_ONE_ITEM;

    gboolean do_bg = FALSE;     /* do background operation? */

    static gboolean i18n_flag = FALSE;
    if (!i18n_flag)
    {
        for (i = G_N_ELEMENTS (op_names); i-- != 0;)
            op_names[i] = Q_ (op_names[i]);
        i18n_flag = TRUE;
    }

    linklist = free_linklist (linklist);
    dest_dirs = free_linklist (dest_dirs);

    save_cwds_stat ();

    if (single_entry)
    {
        source = check_single_entry (panel, force_single, &src_stat);

        if (source == NULL)
            return FALSE;
    }

    ctx = file_op_context_new (operation);

    /* Show confirmation dialog */
    if (operation != OP_DELETE)
    {
        dest = do_confirm_copy_move (panel, force_single, source, &src_stat, ctx, &do_bg);
        if (dest == NULL)
        {
            ret_val = FALSE;
            goto ret_fast;
        }

        dest_vpath = vfs_path_from_str (dest);
    }
    else if (confirm_delete && !do_confirm_erase (panel, source, &src_stat))
    {
        ret_val = FALSE;
        goto ret_fast;
    }

    ctx->total_transfer_start = g_get_monotonic_time ();

#ifdef ENABLE_BACKGROUND
    /* Did the user select to do a background operation? */
    if (do_bg)
    {
        int v;

        v = do_background (ctx,
                           g_strconcat (op_names[operation], ": ",
                                        vfs_path_as_str (panel->cwd_vpath), (char *) NULL));
        if (v == -1)
            message (D_ERROR, MSG_ERROR, _("Sorry, I could not put the job in background"));

        /* If we are the parent */
        if (v == 1)
        {
            mc_setctl (panel->cwd_vpath, VFS_SETCTL_FORGET, NULL);

            mc_setctl (dest_vpath, VFS_SETCTL_FORGET, NULL);
            vfs_path_free (dest_vpath, TRUE);
            g_free (dest);
            /*          file_op_context_destroy (ctx); */
            return FALSE;
        }
    }
    else
#endif /* ENABLE_BACKGROUND */
    {
        const file_entry_t *fe;

        if (operation == OP_DELETE)
            dialog_type = FILEGUI_DIALOG_DELETE_ITEM;
        else if (single_entry
                 && ((fe = panel_current_entry (panel)) == NULL ? FALSE : S_ISDIR (fe->st.st_mode)))
            dialog_type = FILEGUI_DIALOG_MULTI_ITEM;
        else if (single_entry || force_single)
            dialog_type = FILEGUI_DIALOG_ONE_ITEM;
        else
            dialog_type = FILEGUI_DIALOG_MULTI_ITEM;
    }

    /* Initialize things */
    /* We do not want to trash cache every time file is
       created/touched. However, this will make our cache contain
       invalid data. */
    if ((dest != NULL)
        && (mc_setctl (dest_vpath, VFS_SETCTL_STALE_DATA, GUINT_TO_POINTER (1)) != 0))
        save_dest = vfs_path_from_str (dest);

    if ((vfs_path_tokens_count (panel->cwd_vpath) != 0)
        && (mc_setctl (panel->cwd_vpath, VFS_SETCTL_STALE_DATA, GUINT_TO_POINTER (1)) != 0))
        save_cwd = vfs_path_clone (panel->cwd_vpath);

    /* Now, let's do the job */

    /* This code is only called by the tree and panel code */
    if (single_entry)
    {
        /* We now have ETA in all cases */

        /* One file: FIXME mc_chdir will take user out of any vfs */
        if ((operation != OP_COPY) && (get_current_type () == view_tree))
        {
            vfs_path_t *vpath;
            int chdir_retcode;

            vpath = vfs_path_from_str (PATH_SEP_STR);
            chdir_retcode = mc_chdir (vpath);
            vfs_path_free (vpath, TRUE);
            if (chdir_retcode < 0)
            {
                ret_val = FALSE;
                goto clean_up;
            }
        }

        value = operate_single_file (panel, ctx, source, &src_stat, dest, dialog_type);
        if ((value == FILE_CONT) && !force_single)
            unmark_files (panel);
    }
    else
    {
        /* Many files */

        /* Check destination for copy or move operation */
        while (operation != OP_DELETE)
        {
            int dst_result;
            mc_stat_t dst_stat;

            dst_result = mc_stat (dest_vpath, &dst_stat);

            if ((dst_result != 0) || S_ISDIR (dst_stat.st_mode))
                break;

            if (ctx->ignore_all
                || file_error (ctx, TRUE, _("Destination \"%s\" must be a directory\n%s"),
                               dest) != FILE_RETRY)
                goto clean_up;
        }

        /* TODO: the good way is required to skip directories scanning in case of rename/move
         * of several directories. Since reqular expression can be used for destination,
         * some directory movements can be a cross-filesystem and directory scanning is useful
         * for those directories only. */

        value =
            panel_operate_init_totals (panel, NULL, NULL, ctx, file_op_compute_totals, dialog_type);
        if (value == FILE_CONT)
            /* Loop for every file, perform the actual copy operation */
            for (i = 0; i < panel->dir.len; i++)
            {
                const char *source2;

                if (panel->dir.list[i].f.marked == 0)
                    continue;   /* Skip the unmarked ones */

                source2 = panel->dir.list[i].fname->str;
                src_stat = panel->dir.list[i].st;

                value = operate_one_file (panel, ctx, source2, &src_stat, dest);
                if (value == FILE_ABORT)
                    break;

                if (value == FILE_CONT)
                    do_file_mark (panel, i, 0);

                mc_refresh ();
            }                   /* Loop for every file */
    }                           /* Many entries */

  clean_up:
    /* Clean up */
    if (save_cwd != NULL)
    {
        mc_setctl (save_cwd, VFS_SETCTL_STALE_DATA, NULL);
        vfs_path_free (save_cwd, TRUE);
    }

    if (save_dest != NULL)
    {
        mc_setctl (save_dest, VFS_SETCTL_STALE_DATA, NULL);
        vfs_path_free (save_dest, TRUE);
    }

    linklist = free_linklist (linklist);
    dest_dirs = free_linklist (dest_dirs);
    g_free (dest);
    vfs_path_free (dest_vpath, TRUE);
    MC_PTR_FREE (ctx->dest_mask);

#ifdef ENABLE_BACKGROUND
    /* Let our parent know we are saying bye bye */
    if (mc_global.we_are_background)
    {
        /* Send pid to parent with child context, it is fork and
           don't modify real parent ctx */
        ctx->pid = getpid ();
        parent_call ((void *) end_bg_process, ctx, 0);

        vfs_shut ();
        my_exit (EXIT_SUCCESS);
    }
#endif /* ENABLE_BACKGROUND */

  ret_fast:
    file_op_context_destroy (ctx);

    update_panels (UP_OPTIMIZE, UP_KEEPSEL);
    repaint_screen ();

    return ret_val;
}

/* }}} */

/* --------------------------------------------------------------------------------------------- */
/* {{{ Query/status report routines */
/** Report error with one file */
FileProgressStatus
file_error (file_op_context_t *ctx, gboolean allow_retry, const char *format, const char *file)
{
    char buf[BUF_MEDIUM];

    g_snprintf (buf, sizeof (buf), format, path_trunc (file, 30), unix_error_string (errno));

    return do_file_error (ctx, allow_retry, buf);
}

/* --------------------------------------------------------------------------------------------- */

/*
   Cause emacs to enter folding mode for this file:
   Local variables:
   end:
 */
