/* vifm * Copyright (C) 2001 Ken Steen. * Copyright (C) 2011 xaizek. * * This program 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 2 of the License, or * (at your option) any later version. * * This program 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include "fops_cpmv.h" #include <assert.h> /* assert() */ #include <string.h> /* strcmp() strdup() */ #include "compat/reallocarray.h" #include "modes/dialogs/msg_dialog.h" #include "ui/cancellation.h" #include "ui/fileview.h" #include "ui/statusbar.h" #include "utils/fs.h" #include "utils/path.h" #include "utils/str.h" #include "utils/string_array.h" #include "filelist.h" #include "flist_pos.h" #include "fops_common.h" #include "fops_misc.h" #include "ops.h" #include "trash.h" #include "undo.h" /* Arguments pack for fops_query_list() verification function. */ typedef struct { view_t *view; /* Source view. */ const char *dst_path; /* Destination directory. */ int force; /* Whether name conflicts should be ignored. */ CopyMoveLikeOp op; /* Operation. */ } verify_args_t; static int cp_file(const char src_dir[], const char dst_dir[], const char src[], const char dst[], CopyMoveLikeOp op, int cancellable, ops_t *ops, int force); static int is_erroneous(view_t *view, const char dst_dir[], int force); static int cpmv_prepare(view_t *view, char ***list, int *nlines, CopyMoveLikeOp op, int ignore_conflicts, char undo_msg[], size_t undo_msg_len, char dst_path[], size_t dst_path_len, int *from_file); static int verify_list(char *files[], int nfiles, char *names[], int nnames, char **error, void *data); static int check_for_clashes(verify_args_t *args, char *list[], char *marked[], int nlines, char **error); static const char * cmlo_to_str(CopyMoveLikeOp op); static void cpmv_files_in_bg(bg_op_t *bg_op, void *arg); static void set_cpmv_bg_descr(bg_op_t *bg_op, bg_args_t *args, size_t i); static void cpmv_file_in_bg(ops_t *ops, const char src[], const char dst[], int move, int force, int skip, int from_trash, const char dst_dir[]); static int cp_file_f(const char src[], const char dst[], CopyMoveLikeOp op, int bg, int cancellable, ops_t *ops, int force); int fops_cpmv(view_t *view, char *list[], int nlines, CopyMoveLikeOp op, int flags) { const int force = (flags & CMLF_FORCE); const int skip = (flags & CMLF_SKIP); int err; int nmarked_files; int custom_fnames; int i; char undo_msg[COMMAND_GROUP_INFO_LEN + 1]; dir_entry_t *entry; char dst_dir[PATH_MAX + 1]; int from_file; ops_t *ops; if((op == CMLO_LINK_REL || op == CMLO_LINK_ABS) && !symlinks_available()) { show_error_msg("Symbolic Links Error", "Your OS doesn't support symbolic links"); return 0; } const int ignore_conflicts = (force || skip); err = cpmv_prepare(view, &list, &nlines, op, ignore_conflicts, undo_msg, sizeof(undo_msg), dst_dir, sizeof(dst_dir), &from_file); if(err != 0) { return err > 0; } if(is_erroneous(view, dst_dir, force)) { return 0; } switch(op) { case CMLO_COPY: ops = fops_get_ops(OP_COPY, "Copying", flist_get_dir(view), dst_dir); break; case CMLO_MOVE: ops = fops_get_ops(OP_MOVE, "Moving", flist_get_dir(view), dst_dir); break; case CMLO_LINK_REL: case CMLO_LINK_ABS: ops = fops_get_ops(OP_SYMLINK, "Linking", flist_get_dir(view), dst_dir); break; default: assert(0 && "Unexpected operation type."); return 0; } nmarked_files = fops_enqueue_marked_files(ops, view, dst_dir, 0); un_group_open(undo_msg); i = 0; entry = NULL; custom_fnames = (nlines > 0); while(iter_marked_entries(view, &entry) && !ui_cancellation_requested()) { /* Must be at this level as dst might point into this buffer. */ char src_full[PATH_MAX + 1]; char dst_full[PATH_MAX + 8]; const char *dst = (custom_fnames ? list[i] : entry->name); int err, from_trash; int skipped = 0; get_full_path_of(entry, sizeof(src_full), src_full); from_trash = trash_has_path(src_full); if(from_trash && !custom_fnames) { snprintf(src_full, sizeof(src_full), "%s/%s", entry->origin, dst); chosp(src_full); dst = trash_get_real_name_of(src_full); } snprintf(dst_full, sizeof(dst_full), "%s/%s", dst_dir, dst); if(path_exists(dst_full, NODEREF)) { if(skip) { skipped = 1; } else if(force && !from_trash) { (void)perform_operation(OP_REMOVESL, NULL, NULL, dst_full, NULL); } } if(op == CMLO_COPY) { fops_progress_msg("Copying files", i, nmarked_files); } else if(op == CMLO_MOVE) { fops_progress_msg("Moving files", i, nmarked_files); } if(skipped) { /* Nothing to do for this path. */ err = 0; } else if(op == CMLO_MOVE) { err = fops_mv_file(entry->name, entry->origin, dst, dst_dir, OP_MOVE, 1, ops); if(err != 0) { view->list_pos = fpos_find_by_name(view, entry->name); } } else { err = cp_file(entry->origin, dst_dir, entry->name, dst, op, 1, ops, 0); } ops_advance(ops, err == 0); ++i; } un_group_close(); if(flist_custom_active(view) || pane_in_dir(view, dst_dir)) { ui_view_schedule_reload(view); } ui_view_schedule_reload(view == curr_view ? other_view : curr_view); if(from_file) { free_string_array(list, nlines); } ui_sb_msgf("%d file%s successfully processed%s", ops->succeeded, (ops->succeeded == 1) ? "" : "s", fops_get_cancellation_suffix()); fops_free_ops(ops); return 1; } void fops_replace_entry(ops_t *ops, view_t *src, const dir_entry_t *src_entry, view_t *dst, dir_entry_t *dst_entry) { void *cp = (void *)(size_t)1; int dst_exists = !fentry_is_fake(dst_entry); char dst_dir[PATH_MAX + 1]; if(dst_exists) { copy_str(dst_dir, sizeof(dst_dir), dst_entry->origin); } else { const char *src_tail = src_entry->origin + strlen(flist_get_dir(src)); build_path(dst_dir, sizeof(dst_dir), flist_get_dir(dst), src_tail); } char dst_full[PATH_MAX + 1]; build_path(dst_full, sizeof(dst_full), dst_dir, src_entry->name); char src_full[PATH_MAX + 1]; get_full_path_of(src_entry, sizeof(src_full), src_full); if(paths_are_same(src_full, dst_full)) { /* Nothing to do if destination and source are the same file. */ return; } /* Deleting it explicitly instead of letting cp_file() do it to move the file * to trash and make operation reversible. */ if(dst_exists && path_exists(dst_full, NODEREF)) { fops_delete_entry(ops, dst, dst_entry, /*use_trash=*/1, /*nested=*/1); } fops_progress_msg("Copying files", 0, 1); if(fops_active(ops) && !is_valid_dir(dst_dir) && perform_operation(OP_MKDIR, ops, cp, dst_dir, NULL) == OPS_SUCCEEDED) { un_group_add_op(OP_MKDIR, cp, NULL, dst_dir, ""); } if(fops_active(ops)) { /* Not forcing as destination path shouldn't exist. */ if(cp_file_f(src_full, dst_full, CMLO_COPY, /*bg=*/0, /*cancellable=*/1, ops, /*force=*/0) == 0 && !dst_exists) { /* Update the destination entry to not be fake. */ replace_string(&dst_entry->name, src_entry->name); replace_string(&dst_entry->origin, dst_dir); } } ui_view_schedule_reload(dst); } /* Adapter for cp_file_f() that accepts paths broken into directory/file * parts. */ static int cp_file(const char src_dir[], const char dst_dir[], const char src[], const char dst[], CopyMoveLikeOp op, int cancellable, ops_t *ops, int force) { char full_src[PATH_MAX + 1], full_dst[PATH_MAX + 1]; to_canonic_path(src, src_dir, full_src, sizeof(full_src)); to_canonic_path(dst, dst_dir, full_dst, sizeof(full_dst)); return cp_file_f(full_src, full_dst, op, 0, cancellable, ops, force); } int fops_cpmv_bg(view_t *view, char *list[], int nlines, int move, int flags) { const int force = (flags & CMLF_FORCE); const int skip = (flags & CMLF_SKIP); int err; size_t i; char task_desc[COMMAND_GROUP_INFO_LEN]; bg_args_t *args = calloc(1, sizeof(*args)); if(args == NULL) { show_error_msg("Can't perform operation", "Out of memory"); return 0; } args->nlines = nlines; args->move = move; args->force = force; args->skip = skip; const int ignore_conflicts = (force || skip); err = cpmv_prepare(view, &list, &args->nlines, move ? CMLO_MOVE : CMLO_COPY, ignore_conflicts, task_desc, sizeof(task_desc), args->path, sizeof(args->path), &args->from_file); if(err != 0) { fops_free_bg_args(args); return err > 0; } if(is_erroneous(view, args->path, force)) { fops_free_bg_args(args); return 0; } args->list = args->from_file ? list : args->nlines > 0 ? copy_string_array(list, nlines) : NULL; fops_prepare_for_bg_task(view, args); args->is_in_trash = malloc(args->sel_list_len); for(i = 0U; i < args->sel_list_len; ++i) { args->is_in_trash[i] = trash_has_path(args->sel_list[i]); } if(args->list == NULL) { int i; args->nlines = args->sel_list_len; args->list = reallocarray(NULL, args->nlines, sizeof(*args->list)); for(i = 0; i < args->nlines; ++i) { args->list[i] = strdup(fops_get_dst_name(args->sel_list[i], args->is_in_trash[i])); } } args->ops = fops_get_bg_ops(move ? OP_MOVE : OP_COPY, move ? "moving" : "copying", args->path); if(bg_execute(task_desc, "...", args->sel_list_len, 1, &cpmv_files_in_bg, args) != 0) { fops_free_bg_args(args); show_error_msg("Can't process files", "Failed to initiate background operation"); } return 0; } /* Checks operation for adequacy. Displays error message in case of issues. * Returns non-zero if operation must be aborted, otherwise zero is returned. */ static int is_erroneous(view_t *view, const char dst_dir[], int force) { const int same_dir = pane_in_dir(view, dst_dir); if(same_dir && force) { show_error_msg("Operation Aborted", "Forcing overwrite when destination and source is the same directory " "will lead to losing data."); return 1; } return 0; } /* Performs general preparations for file copy/move-like operations: resolving * destination path, validating names, checking for conflicts, formatting undo * message. Returns zero on success, otherwise positive number for status bar * message and negative number for other errors. */ static int cpmv_prepare(view_t *view, char ***list, int *nlines, CopyMoveLikeOp op, int ignore_conflicts, char undo_msg[], size_t undo_msg_len, char dst_path[], size_t dst_path_len, int *from_file) { view_t *const other = (view == curr_view) ? other_view : curr_view; if(op == CMLO_MOVE) { if(!fops_view_can_be_changed(view)) { return -1; } } else if(op == CMLO_COPY && !fops_can_read_marked_files(view)) { return -1; } if(*nlines == 1) { if(fops_check_dir_path(other, (*list)[0], dst_path, dst_path_len)) { *nlines = 0; } } else { copy_str(dst_path, dst_path_len, fops_get_dst_dir(other, -1)); } if(!fops_view_can_be_extended(other, -1) || !fops_is_dir_writable(DR_DESTINATION, dst_path)) { return -1; } size_t nmarked; char **marked = fops_grab_marked_files(view, &nmarked); /* Custom views can contain several files with the same name. */ if(*nlines == 0 && flist_custom_active(view)) { size_t i; for(i = 0U; i < nmarked; ++i) { if(is_in_string_array(marked, i, marked[i])) { ui_sb_errf("Source name \"%s\" duplicates", marked[i]); free_string_array(marked, nmarked); return 1; } } } verify_args_t verify_args = { .view = view, .dst_path = dst_path, .force = ignore_conflicts, .op = op, }; *from_file = (*nlines < 0); if(*from_file) { *list = fops_query_list(nmarked, marked, nlines, 1, &verify_list, &verify_args); if(*list == NULL) { free_string_array(marked, nmarked); return -1; } } char *error_str = NULL; int error = !verify_list(marked, nmarked, *list, *nlines, &error_str, &verify_args); free_string_array(marked, nmarked); if(error) { if(error_str != NULL) { ui_sb_err(error_str); free(error_str); } redraw_view(view); if(*from_file) { free_string_array(*list, *nlines); } return 1; } snprintf(undo_msg, undo_msg_len, "%s from %s to ", cmlo_to_str(op), replace_home_part(flist_get_dir(view))); snprintf(undo_msg + strlen(undo_msg), undo_msg_len - strlen(undo_msg), "%s: ", replace_home_part(dst_path)); fops_append_marked_files(view, undo_msg, (*nlines > 0) ? *list : NULL); return 0; } /* Checks that operation can be performed. Returns non-zero if so, otherwise * zero is returned along with setting *error. */ static int verify_list(char *files[], int nfiles, char *names[], int nnames, char **error, void *data) { verify_args_t *args = data; int ok = 1; if(nnames > 0 && (!fops_is_name_list_ok(nfiles, nnames, names, NULL, error) || !fops_is_copy_list_ok(args->dst_path, nnames, names, args->force, error))) { ok = 0; } else if(nnames == 0 && !fops_is_copy_list_ok(args->dst_path, nfiles, files, args->force, error)) { ok = 0; } if(ok && check_for_clashes(args, names, files, nnames, error) != 0) { ok = 0; } return ok; } /* Checks whether operation is OK from the point of view of losing files due to * tree clashes (child move over parent or vice versa). Reallocates *error to * provide error message. Returns zero if everything is fine, otherwise * non-zero is returned. */ static int check_for_clashes(verify_args_t *args, char *list[], char *marked[], int nlines, char **error) { const int is_cpmv = ONE_OF(args->op, CMLO_MOVE, CMLO_COPY); dir_entry_t *entry = NULL; int i = 0; while(iter_marked_entries(args->view, &entry)) { char src_full[PATH_MAX + 1], dst_full[PATH_MAX + 1]; const char *const dst_name = (nlines > 0) ? list[i] : marked[i]; ++i; get_full_path_of(entry, sizeof(src_full), src_full); snprintf(dst_full, sizeof(dst_full), "%s/%s", args->dst_path, dst_name); chosp(dst_full); if(is_cpmv && is_in_subtree(dst_full, src_full, 0)) { put_string(error, format_str("Can't move/copy parent inside itself: %s", replace_home_part(src_full))); return 1; } if(is_in_subtree(src_full, dst_full, 0)) { put_string(error, format_str("Operation would result in losing contents of %s", replace_home_part(src_full))); return 1; } } return 0; } /* Gets string representation of a copy/move-like operation. Returns the * string. */ static const char * cmlo_to_str(CopyMoveLikeOp op) { switch(op) { case CMLO_COPY: return "copy"; case CMLO_MOVE: return "move"; case CMLO_LINK_REL: return "rlink"; case CMLO_LINK_ABS: return "alink"; default: assert(0 && "Unexpected operation type."); return ""; } } /* Entry point for a background task that copies/moves files. */ static void cpmv_files_in_bg(bg_op_t *bg_op, void *arg) { size_t i; bg_args_t *const args = arg; ops_t *ops = args->ops; fops_bg_ops_init(ops, bg_op); if(ops->use_system_calls) { size_t i; bg_op_set_descr(bg_op, "estimating..."); for(i = 0U; i < args->sel_list_len; ++i) { const char *const src = args->sel_list[i]; const char *const dst = args->list[i]; ops_enqueue(ops, src, dst); } } for(i = 0U; i < args->sel_list_len; ++i) { const char *const src = args->sel_list[i]; const char *const dst = args->list[i]; set_cpmv_bg_descr(bg_op, args, i); cpmv_file_in_bg(ops, src, dst, args->move, args->force, args->skip, args->is_in_trash[i], args->path); ++bg_op->done; } fops_free_bg_args(args); } /* Sets nice title for the background job. */ static void set_cpmv_bg_descr(bg_op_t *bg_op, bg_args_t *args, size_t i) { const char *src = args->sel_list[i]; /* Start with the source path. */ const char *descr = src; /* Try shorten the path using tilde and use that if it worked. */ char *tilde_src = make_tilde_path(src); if(tilde_src != NULL) { descr = tilde_src; } /* In case there are multiple files to process, prepend current position. */ char *stats = NULL; if(args->sel_list_len > 1) { /* The space is doubled for the symmetry with progress appended by the job * bar. */ stats = format_str("%d/%d %s", (int)i + 1, (int)args->sel_list_len, descr); } if(stats != NULL) { descr = stats; } bg_op_set_descr(bg_op, descr); free(tilde_src); free(stats); } /* Actual implementation of background file copying/moving. */ static void cpmv_file_in_bg(ops_t *ops, const char src[], const char dst[], int move, int force, int skip, int from_trash, const char dst_dir[]) { char dst_full[PATH_MAX + 1]; snprintf(dst_full, sizeof(dst_full), "%s/%s", dst_dir, dst); if(path_exists(dst_full, NODEREF)) { if(skip) { return; } if(force && !from_trash) { (void)perform_operation(OP_REMOVESL, NULL, (void *)1, dst_full, NULL); } } if(move) { (void)fops_mv_file_f(src, dst_full, OP_MOVE, 1, 1, ops); } else { (void)cp_file_f(src, dst_full, CMLO_COPY, 1, 1, ops, 0); } } /* Copies file from one location to another. Returns zero on success, otherwise * non-zero is returned. */ static int cp_file_f(const char src[], const char dst[], CopyMoveLikeOp op, int bg, int cancellable, ops_t *ops, int force) { if(strcmp(src, dst) == 0) { return 0; } OPS file_op; char rel_path[PATH_MAX + 1]; if(op == CMLO_COPY) { file_op = force ? OP_COPYF : OP_COPY; } else { file_op = OP_SYMLINK; if(op == CMLO_LINK_REL) { char dst_dir[PATH_MAX + 1]; copy_str(dst_dir, sizeof(dst_dir), dst); remove_last_path_component(dst_dir); copy_str(rel_path, sizeof(rel_path), make_rel_path(src, dst_dir)); src = rel_path; } } OpsResult result = perform_operation(file_op, ops, cancellable ? NULL : (void *)1, src, dst); if(result != OPS_SUCCEEDED) { return 1; } if(!bg) { un_group_add_op(file_op, NULL, NULL, src, dst); } return 0; } /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */ /* vim: set cinoptions+=t0 : */